Daniel Lange: Getting AVIF thumbnails in XFCE4 thunar (Debian Trixie)

PlanetDebian
daniel-lange.com
2026-09-07 04:50:00
The AVIF image format gets more and more popular in the web dev community, so I needed to teach XFCE4's thunar (file manager) and Ristretto (image viewer) to thumbnail these. Luckily that is not too hard: Debian Trixie separates its gdk-pixbuf libraries slightly differently than previous versions....
Original Article

Debian Internet

The AVIF image format gets more and more popular in the web dev community, so I needed to teach XFCE4's thunar (file manager) and Ristretto (image viewer) to thumbnail these.

Luckily that is not too hard:

Debian Trixie separates its gdk-pixbuf libraries slightly differently than previous versions. That's why it is not "automatically there". Ensure you have the libavif-gdk-pixbuf plugin and the tumbler service (which XFCE uses to process thumbnails):

sudo apt --update install libavif-gdk-pixbuf tumbler

Thunar has likely tried (and failed) to load your AVIF files before you installed the package, it will have saved a blank or "broken image" placeholder in a thumbnail cache directory. It will not attempt to regenerate them unless you clear this cache:

# Clear the thumbnail cache
rm -rf ~ / .cache / thumbnails /*

# Force-quit thunar and the tumblerd background service
thunar -q
pkill tumblerd

Tumbled will restart on its own when it is needed. When you open thunar again and navigate to your image directory ... your AVIF images will now generate thumbnails automatically like the other image format did already.

Avif thumbnails in thunar

"Hammock Driven Development" (2010)

Lobsters
youtu.be
2026-09-07 04:31:19
Comments...

Programming is Art

Hacker News
orchidfiles.com
2026-09-07 04:05:00
Comments...
Original Article

When I started programming, my whole day revolved around development. I’d wake up, write code, and go to sleep. And that’s how it was day in and day out. My entire social circle consisted of people who also wrote code. I studied frameworks, libraries, and various programming languages.

I got into development because I was fascinated by the idea that by writing code, I could create new programs. All I needed to do was learn how to program, and I’d be able to create anything. If I learned PHP, I could build back-end applications. If I learned HTML and CSS, I could build websites.

I could choose what I wanted to create, learn the necessary technologies for it, and start building. You open an IDE, create a new project, and build it from scratch. You choose patterns, libraries, and frameworks; you read the documentation and dive into the source code. You get so immersed in the process that you don’t even notice how the days fly by. Over time, you develop your own coding style.

I wrote code, and I enjoyed writing code, but it wasn’t the kind of work I wanted to do for the rest of my life. I preferred creating something new to the actual process of writing code. I liked starting from scratch — with nothing but an idea — to create a product that people actually use. That’s why I’ve always worked exclusively at startups and never at large companies.

I’ve always dreamed of advancing my career — to become a team lead, a manager, or to take on another role that didn’t involve writing code. And whenever someone else could write the code, I’d shift away from coding toward managerial tasks. Whether I wrote the code myself or delegated it to others didn’t matter to me.

Although I got into development out of an interest in creating things, later on I did it solely because I was getting paid for it. For me, it was just a job. Even though I wrote code in my free time, I still used that approach to build my own startups, which I planned to make money from. Writing code and making money were inseparable. Over time, income from projects became the main focus, rather than simply the desire to create projects.

But there’s another type of person. These are true programmers who write code regardless of income. They write code simply because they can’t help but write it. It doesn’t matter to them how much money they make from it. They might work on open-source projects that will never generate any income for them.

They enjoy writing code, solving development challenges, designing, figuring out the logic, fixing bugs, and understanding the entire system as a whole. They enjoy the process itself. And they’ll never hand that over to AI agents.

I love building startups, and they love writing code. For me, building projects is a way to make money. For them, programming is art.

That’s why all my arguments about how AI helps me build projects are completely irrelevant to true artists. They don’t need AI to help them with anything.

And that’s why there’s often a negative reaction to the claim that AI will soon replace all programmers. For artists, this claim itself is illogical. Even if some technological breakthrough occurs and AI can truly work autonomously and solve problems hundreds of times faster than humans, people will still continue to write code by hand, line by line. AI will never be able to take away their ability and desire to write code.

I don’t use AI when writing. It produces soulless, bland text. AI slop that no one wants to read. I want to come up with the wording myself, proofread and edit the text, come up with a title, and delete unnecessary sentences and entire paragraphs. I like it when new ideas for the text come to me as I’m writing. The writing process itself — the thinking that goes on in the moment — is important to me, not just the result. If I asked an AI to write a text on the topic “Programming is art”, that thought process wouldn’t happen. I wouldn’t experience the same emotions that come with writing it myself. For me, writing is art that I pursue regardless of money. I think people who enjoy writing code feel the same way.

Subscribe

It's time for Mark Zuckerberg to resign from Meta

Hacker News
www.theguardian.com
2026-09-07 03:59:21
Comments...
Original Article

L ast week, Meta, the parent company of Instagram and Facebook, agreed to a settlement of up to $18bn after dozens of US states accused it of harming children with addictive products. The settlement’s regulatory implications for social media will affect every user, far beyond teens.

But if Meta is truly interested in prioritizing the safety of users, the board should request the resignation of Mark Zuckerberg and restructure to put safety above profit.

The settlement does little to resolve the scientific question about the degree to which social media is addictive, but instead is the culmination of a battle launched by journalists and independent researchers in 2016 that focused on the digital design of disinformation campaigns powering Brexit in the UK and the US presidential election. Zuckerberg is the one throughline in Meta’s history of billion-dollar blunders that have hurt real people.

For the past 15 years, I have studied how science and technology produces social change, for better and worse . As the founder of The Critical Internet Studies Institute , I lead a team investigating harms caused by emerging technologies. We educate practitioners and professionals about the manifest and latent features of new technologies, warts and all . In 2020, I testified to Congress about misinformation as “secondhand smoke”. My comments came weeks after testimony from Facebook’s former director of monetization, who likened the design process of making Meta’s engagement optimization algorithms to the ways tobacco companies made cigarettes more addictive.

In a series of op-eds, congressional briefings and research papers, I extended the tobacco case study to show how cultural attitudes about smoking as a personal luxury were upended by concepts such as secondhand smoke, and scientific confirmation that tobacco increases risk of cancer, whose true costs were passed on to families, insurance companies, and employers who lost time and resources to combatting smoking. The public health solution did not go as far as to ban smoking, but limited the areas where smoking was allowed, set an age limit for possession, made companies pay for public education campaigns on the hazards of the product, and most importantly, put products behind grumpy cashiers who validate IDs to purchase. Public health campaigns and warning labels were also important strategies.

Similarly, social media companies make all kinds of critical decisions about how to deliver content to your feed. Some of those features are algorithmically engineered to increase the likelihood of users remaining on the app.

The settlement comes with design restrictions ; teen accounts are limited to two hours per day, no access from midnight to 6am, and no notifications from school bell to bell. Teen accounts must be linked to a parent’s accounts, amid stronger age verification. It’s still unclear how that will be implemented, but it promises to become a privacy nightmare. While these interventions limit time for 13-18 year olds, they do little to address contentious content, such as how drugs, sex and gambling end up algorithmically recommended.

It is clear that this latest settlement is the harbinger of a punishing future ahead for Zuckerberg’s businesses. Meta has litigated a number of issues, while avoiding any serious regulation on content moderation (cost center) or advertising (profit center). Enforcing these new rules for teens will increase costs. Meta has faced billions in settlements or fines for harms ranging from failing to protect user privacy to a 2018 security breach to abhorrent outcomes for young people’s mental health . Meta has also caused unremunerated harms to other groups, including amplifying hate speech against Rohingya in Myanmar and hosting and algorithmically promoting rampant medical misinformation and conspiracy theories leading to toxic poisoning and vaccine hesitancy that the World Health Organization labelled an “ infodemic ”.

Zuckerberg’s leadership failures directly led to well-documented damage to civic life, democracy, and global mental and physical health and wellbeing, especially that of young people. Since founding Facebook in 2004, Zuckerberg has been in constant litigation over a number of issues related to ownership and stock early on, which is where he first started to employ a PR strategy to delay, deny and deflect any issues that arise from executives’ decisions to put profit over users’ health. Meta executives lined their pockets off of the tragic and devastating amplification of hate, harassment and incitement.

Meanwhile, it has produced a cadre of whistleblowers, with more to come now that it’s clear these executives’ choices led to “real-world harm”, a Silicon Valley term of art that separates cyberspace from meatspace.

The settlement comes on the heels of last week’s damning hearings, where one of those whistleblowers, Arturo Béjar, boldly stated : “You just cannot trust Mark Zuckerberg with kids.” Béjar was a lead on safety at Instagram when his teen daughter was the victim of unwanted sexist harassment and profanity on Instagram. Because of his role at Meta, he had real data for accurate measurements of harms on Instagram including the effectiveness of different algorithmic interventions on growth, engagement and user experience.

Béjar estimated he had met with Zuckerberg more than 100 times before becoming a whistleblower. Many whistleblowers’ stories begin with an ardent attempt to fix the problems through the correct channels and a refusal from higher-ups to tackle the issue. Ironically, whistleblowers tend to be rule followers who become increasingly indignant as their concerns are met with a workplace culture of ambivalence or advance compliance.

In December 2023, I experienced intense professional blowback in academia after obtaining documents culled from another whistleblower, Frances Haugen, which I intended to publish in a publicly accessible database. I acquired the documents legally and used my expertise to help tech insiders, journalists, researchers and Washington DC staffers understand what the Haugen documents did, and importantly did not, say about Meta’s liability and knowledge of product problems. But the message was clear: Meta, then called Facebook, knew of the damage Instagram caused to teenage girls.

And yet, through every scandal, Zuckerberg’s net worth has climbed to nearly $200bn , according to Forbes. Fines mean nothing to those with wealth like his, so the first step to safeguard society is to give Meta a fresh start without Zuckerberg’s influence on future products. His vision for “a future for everyone” is an incredibly facile utopia, where AI agents run your life and you just slop your way through it.

Beyond the courts, Meta could triage the situation by calling for Zuckerberg to voluntarily resign. Ushering in hope for the future of technology requires changing Meta’s leadership now, so that the future is collectively imagined and built upon a new reputation and commitment.

Zuckerberg does not deserve another chance simply because he can afford it. We as a society ultimately pay for his mistakes. Now is the time for shareholders to speak up and push back on Zuckerberg’s dominance and control.

  • Joan Donovan is an assistant professor of journalism at Boston University who studies how technology impacts society. She is the founder of the Critical Internet Studies Institute , where she leads a project on political violence and social movements

Internationalization and Localization

Lobsters
www.kashyapsuhas.com
2026-09-07 02:52:49
Comments...
Original Article

Consider this scenario: You're a person from the US, staying in Canada, and looking to book a hotel room in Jaipur, India from my Travel site. You're looking at a card like this:

Jaipur grand hotel --- 04/05 -> 04/07 --- $1,12,107.43 --- फ्री कैंसिलेशन

"ok, why is 2 days of stay showing up as a million Dollars?"
"what does it say in Hindi?"
"wait, that's not a million, why are the commas all wrong?"
"is the rate in Canadian Dollars or US Dollars?"

"aaah I'm going to book from a different website!"

If my site happened to have supported good localization (l10n), you would've switched to the exact format that you'd understand, and seen this card instead:

Jaipur grand hotel --- 05/04 -> 07/04 --- CAD 112,107.43 --- Free cancellation

You'd realise that you had accidentally chosen 2 months of stay instead of 2 days, that the currency was in Canadian Dollars, and that it said "Free cancellation". And perhaps would've continued booking a room on my Travel OTA [ 1 ] .

This is a small glimpse into why supporting l10n is super important if you want to take your product global. i18n or Internationalization is the codebase architecture set up part of supporting l10n.

Step 0

Never hardcode user-facing strings. Always pass them through a localization function.

An important note is to also never force preferences onto a user. For example, a person located in Japan could be a tourist and not be able to understand Japanese text. Or an Indian located in India may not be able to understand Hindi. Always give the user an option to change their interface localisation settings, and make the setting obvious and accessible.

Translation

This one starts easy. A bunch of JSON files per key, categorised and boxed into directories based on function, think Listing.Cancellation.Free_Cancellation , with sub-keys being languages and then the values.

One thing to note here is that you should probably not load all the translations into memory, as this is going to end up being very very fat as you scale.

Locale Listing.Cancellation.Free_Cancellation
en-US Free Cancellation
en-IN Free Cancellation
hi-IN फ्री कैंसिलेशन
ko-KR 예약 무료 취소

Conditionals

Complexity starts when you realise languages have their own rules for plurals, grammar, etc. Apart from not making the silly mistake of adding an "s" at the end, a way to solve this is to have conditions in your translations. This way you can abstract away conditional complexity into your translation function instead of muddying your code.
Think something like this:

// this is a terrible idea
const label = `Book ${roomCount} room${roomCount === 1 ? "" : "s"}`;

// try something like
<Translate id="Booking.Confirmation.Book_rooms" count={roomCount} />;
{
  "Booking.Confirmation.Book_rooms": [
    {
      "condition": "count === 1",
      "value": [
        { "lang": "en-US", "value": "Book {count} room." },
        { "lang": "ja-JP", "value": "部屋を{count}部屋予約する。" }
      ]
    },
    {
      "condition": "count > 1",
      "value": [
        { "lang": "en-US", "value": "Book all {count} rooms." },
        { "lang": "ja-JP", "value": "{count}部屋予約する。" }
      ]
    }
  ]
}

The conditions can also be for other booleans and enums.

Layout

Then come text length and layout considerations. You'd ideally want to let your components grow with padding and margins instead of tight fixed widths.

Locale Translation
ko-KR 123 조회
en-US 123 views
it-IT 123 visualizzazioni

Similarly, with truncation, you should consider if the text is important information or not, and let it perhaps wrap to the next line gracefully.

RTL (Arabic, Hebrew) is a whole different beast that I'm going to skip at the moment.

CJK Zenkaku

When Keyboard layout is set to these, afaik the characters that are output use "Zenkaku" or extra-width, such that the individual characters are as wide as a typical Lating letter.
The problem is when a CJK user types in English in the middle, all your validation is going to fail!

Type Narrow / halfwidth Fullwidth
Letters and digits ABC123 ABC123
Katakana カタカナ カタカナ

Numbers

Numbers of any kind must always pass through a formatter function.
Numbers can be monetary values, distances, ratings, percentages.

Example keys in your config to save: measurementSystem , numberSeparator.decimals , currency.decimalPlaces , currency.symbol.domestic .

Separator

Comma separators are different in different countries, and yes they're not always commas!

Many countries group digits in threes, some European ones swap commas with dots, and India has its own lakh and crore system.

Locale Separation
en-US 1,234,567.89
de-DE 1.234.567,89
en-IN 12,34,567.89
fr-FR 1 234 567,89

Money

Money notation differs by placement of currency symbols, space between symbol and amount, local-global symbols, and fraction amount or the lack of.

Some currencies have a local version - Japanese use "円" locally, while yen is denoted by "¥" internationally. Also, many countries using dollars use "$" locally, but for an international traveler it's important to know which dollar they are looking at - USD, SGD, HKD, etc.
The way to sort this out is to typically show the local currency symbol if a combination of market, language, and currency matches a default.

It's also important to let users choose their currency regardless of where they are located. For example, Indians would generally prefer to read a number in a 2,2,3.2 format.

// assume standard value in db is USD.
const systemAmount = 123456.78;
const formattedAmount = l10nMoney(systemAmount, locale, currency);
Locale Currency Display
en-US USD $123,456.78
en-IN INR ₹1,16,65,492.87
de-DE EUR 106.300,61 €
fr-FR EUR 106 300,61 €
en-JP JPY ¥19,253,084
ja-JP JPY 19,253,084円
vi-VN VND 3.217.282.465 ₫

Dates and times

Back to our 2 months vs 2 days confusion. Some countries use DD/MM, some MM/DD. Also, some use an AM/PM format, while others a 24-hr one.
It's generally a good idea to also denote what timezone (IST, JST, PST) a time is in, so that it's super clear (think: should check-in time be user's timezone or hotel's timezone?).

For hours some prefer HH , some H , etc etc. There's also meridians: some countries have their own version of "AM" and "PM" and their placements!

Locale Display
en-US 05/04, 03:30 PM
en-GB 04/05, 15:30
ja-JP 05/04 15:30
vi-VN 04/05, CH 3:30

Countries also have their own first day of the week, what's considered a weekend, and what have you.

Names and Addresses

Japan, China, and few other Asian countries follow a lastName, firstName system, while rest of the world follows the opposite.
Also addresses and the order in which country, zip or postcode, street, state or "prefecture" are presented varies too!

Country Postal Field Example
USA ZIP code 02108
UK Postcode SW1A 1AA
India PIN code 560001

These are even more important when setting up your input forms - think a numeric regex validator for postcodes. Would fail for the UK.

Country State Equivalent
USA State
Canada Province
Japan Prefecture
Russia Oblast
UAE Emirate

These are from a codebase perspective. I should perhaps write a bit about how to set your architecture up to be performant with allll the permutations and combinations. Non-technical stuff is not covered here (think legal requirements like GDPR and other EU laws).

Hope this was helpful. If you know of a unique format that I've missed here, let me know!

  1. [ 1 ]

    OTA: Online Travel Agency. A web based platform to book a hotel room / flight / taxi etc.

N-able patches max severity N-central flaw amid ongoing attacks

Bleeping Computer
www.bleepingcomputer.com
2026-09-07 02:17:41
N-able has released an emergency hotfix for a maximum-severity remote code execution (RCE) flaw affecting its N-central remote monitoring and management (RMM) platform. [...]...
Original Article

N-able

N-able has released an emergency hotfix for a maximum-severity remote code execution (RCE) flaw affecting its N-central remote monitoring and management (RMM) platform.

IT departments and managed service providers (MSPs) use the N-central platform to monitor, manage, and maintain client networks and devices from a centralized web-based console.

Tracked as CVE-2026-86218 , this RCE vulnerability allows threat actors without privileges to execute malicious code on unpatched N-central instances exposed online in low-complexity attacks.

N-able addressed the flaw on Saturday by releasing N-central 2026.3 Hotfix 4 and urging customers to patch as soon as possible.

"At this time, we have no confirmations that this vulnerability has been exploited in production environments, but unpatched systems remain at risk," the company said .

"Customers running on-premises N-central deployments should upgrade to N-central 2026.3 HF4 immediately to protect their environment."

Internet security nonprofit Shadowserver Foundation now tracks nearly 1,500 N-central servers exposed online , most of them located in the United States and Europe.

Internet-exposed N-able instances
Internet-exposed N-able N-central instances (Shadowserver)

Evidence of active exploitation​

While N-able has yet to confirm that the CVE-2026-86218 flaw is being targeted, cybersecurity company Huntress has flagged it as a potential zero-day, along with two high-severity vulnerabilities (tracked as CVE-2026-86206 and CVE-2026-86207, and also patched over the weekend ) that can allow attackers to bypass authentication and gain full access to the vulnerable N-central platform.

"In our 9/5/26 update [..], we had said we could not rule out whether the two previous vulnerabilities released ( CVE-2026-86206 and CVE-2026-86207 ) were the ones that were exploited in the instance seen in the patched production environment of one of our customers," Huntress said.

"Because logs on the compromised N-central server had already rotated, we are also unable to say whether this new CVE was the vulnerability exploited in that case."

"On-premises N-central users must apply HF4 immediately, as systems running HF3 remain vulnerable to this newly disclosed flaw," Huntress warned.

One year ago, N-able released security updates for two N-central vulnerabilities (CVE-2025-8875 and CVE-2025-8876) that attackers were exploiting in the wild .

Days later, Shadowserver found that 880 N-central servers were still vulnerable to attacks exploiting the two security flaws even after CISA ordered federal agencies to patch their systems within a week and urged all security teams to also prioritize securing their systems against ongoing attacks.

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

Sunday Science – Inside the D.O.J.’s Investigation of Fauci and Other Virus Scientists

Portside
portside.org
2026-09-07 02:01:58
Sunday Science – Inside the D.O.J.’s Investigation of Fauci and Other Virus Scientists Ira Mon, 09/07/2026 - 02:01 ...
Original Article

President Trump and Dr. Anthony S. Fauci were shown models of the coronavirus during a tour of the National Institutes of Health in March 2020. | Doug Mills/The New York Times

In early 2025, Dan Bongino, the fiery No. 2 official at the F.B.I., summoned dozens of agents to the bureau’s Washington headquarters.

Mr. Bongino, a conservative media personality recently elevated to the deputy’s role, demanded to know why a yearslong inquiry into the origins of the Covid-19 pandemic had stalled — and why investigators had not more forcefully scrutinized the role of government scientists, current and former officials said.

At one point, he squared off against an official who questioned the theory that he and many supporters of President Trump had long promoted: that the virus had not merely leaked from a Chinese lab, but that American researchers, among them Dr. Anthony S. Fauci, had orchestrated a conspiracy to cover up that fact.

The meeting became so heated that Mr. Bongino ultimately asked the official to leave the room, an attendee said.

Mr. Bongino, who stepped down from the F.B.I. in January to return to podcasting, and other officials helped set in motion a sprawling and aggressive investigative effort that has outlasted his tenure, The New York Times has learned.

As part of that effort, F.B.I. agents and federal prosecutors have sought evidence related to at least eight scientists, among them a Nobel laureate and influential former government researchers like Dr. Fauci.

Investigators have demanded phone, email, grant and payment records, as well as grand jury testimony by scientists.

This account of the investigations is drawn from interviews with six current and former officials familiar with them, who asked not to be identified because they were not authorized to speak publicly, as well as documents reviewed by The Times.

It is not clear what evidence those inquiries have produced or what their status is. Dr. Fauci, once the government’s chief infectious disease scientist, received a pre-emptive pardon last year from former President Joseph R. Biden Jr. that protects him from federal prosecution for official actions from 2014 to 2025.

The inquiries were underway well before Republican lawmakers last month asked the Department of Justice to investigate Dr. Fauci’s refusal to answer questions before a Senate panel. They have continued playing out behind the scenes as those lawmakers released documents that have bruised Dr. Fauci’s reputation.

Dan Bongino, former deputy director of the F.B.I., at a news conference in Washington, flanked by former Attorney General Pam Bondi and the F.B.I. director, Kash Patel. Tierney L. Cross/The New York Times

Many scientists see the federal government’s investigative effort as an attempt to punish political enemies and deflect blame from the first Trump administration for the catastrophic effects of the pandemic.

While the origins of the coronavirus have not definitively been established, these researchers point to the fact that the inquiries have failed to produce any clear evidence that Dr. Fauci or other American scientists covered up knowledge of a lab leak, or funded or collaborated on research that led to one.

Mr. Trump’s supporters, on the other hand, accuse those scientists of helping to start the pandemic and aggravating its harms, and believe the inquiries represent overdue accountability.

The F.B.I.’s efforts during the Biden administration focused mainly on whether Covid emerged from a lab in Wuhan, China, though federal investigators also began examining some individual scientists.

Under Mr. Trump, the investigation has expanded as federal agents and prosecutors sought evidence against new targets and intensified their scrutiny of scientists.

Last month, Dr. David Morens, a former adviser to Dr. Fauci at the National Institutes of Health, pleaded guilty to conspiring to conceal federal records by steering government emails into a personal account. In those emails, Dr. Morens discussed efforts to defend researchers who were under suspicion for collaborating with the Wuhan lab.

Dr. Morens wrote that he was trying to protect researchers from what he described as baseless “political charges” of a lab leak.

A subpoena for documents in that case, dated September 2025 and reviewed by The Times, showed that federal prosecutors in Maryland sought evidence about Dr. Fauci, too, including any emails he sent to certain researchers from his personal account or payments he received for intervening on behalf of federal grantees.

Prosecutors in Maryland also recently demanded the testimony before a federal grand jury of a former staff member at EcoHealth Alliance, a defunct nonprofit that the Trump White House has said used N.I.H. funding to facilitate “dangerous” research at the Wuhan lab where the administration believes the pandemic started.

EcoHealth executives have denied those claims. The N.I.H. has said that viruses being studied there with American funding bore no resemblance to the one that set off the pandemic.

In a separate line of inquiry, agents in the F.B.I.’s Cleveland office have recently examined whether Dr. Fauci and other government scientists pressured virologists in early 2020 to downplay the possibility that the pandemic originated in a lab. Mr. Bongino last year singled out one of those agents as a key player in an expanded investigation.

The F.B.I. and Department of Justice declined to comment for this article. Mr. Bongino conceded on his podcast last month that “there are a lot of complications with any case that may develop against Anthony Fauci because of the pardon,” even as he credited “the cleanup operation we instituted when we went over there” for Dr. Morens’s indictment.

Dr. David Morens, a former N.I.H. official, leaving court in Greenbelt, Md., in May, following his arraignment on charges of evading federal records requests related to Covid research. Annabelle Gordon/Reuters

On his podcast in 2024, Mr. Bongino said that government scientists had dismissed the lab leak theory because they wanted to undermine Mr. Trump.

“They knew that would be a destructive narrative for them because Donald Trump had been talking about the danger of China the entire time,” Mr. Bongino said. “So they hid the lab leak.”

Shortly after Mr. Trump returned to office last year, his administration pulled down a website meant to help Americans find Covid vaccines and treatments and, in its place, posted a full-length image of Mr. Trump atop a new page that described why true blame for the pandemic rested with scientists.

“LAB LEAK,” the page declared — a leak that it suggested Dr. Fauci, Dr. Morens and EcoHealth scientists had helped to conceal.

The National Institute of Allergy and Infectious Diseases, which Dr. Fauci ran for nearly 40 years, financed collaborations between EcoHealth and the Wuhan Institute of Virology, a lab that studied coronaviruses in the city where the Covid outbreak began.

Some researchers favor the idea of a lab leak, whether or not American funding was involved, citing assertions by intelligence officials that the lab probably failed to use adequate biosafety precautions at least some of the time.

Most scientists favor a so-called natural origin theory, pointing to new data published in recent years supporting the idea that the virus spilled from animals into people at a wild animal market in Wuhan, a tried-and-true route for emerging deadly viruses, including past outbreaks in China.

American intelligence officials have said there was no indication that the Wuhan lab had been in possession of the pandemic coronavirus “or a close progenitor” before the Covid outbreak.

In Congress, Republican lawmakers began gathering evidence against scientists as the 2024 election approached.

“You make a pyramid,” said Mitchell Benzine, the staff director of the Republican-run Select Subcommittee on the Coronavirus Pandemic from 2023 to 2025, describing the panel’s strategy. “You talk to and get documents from the bottom of the pyramid, and you work your way up.”

If Dr. Fauci was at the top of the pyramid, Dr. Morens, his former adviser, was nearer its base. The subcommittee discovered a trove of brash emails from 2020 and 2021 in which Dr. Morens described plans to evade federal records laws, including by learning how to “make emails disappear.”

Dr. Anthony S. Fauci, former director of the National Institute of Allergy and Infectious Diseases, declined to answer questions at a Senate committee hearing in July. Haiyun Jiang/The New York Times

Dr. Morens wrote about trying to quietly involve Dr. Fauci in efforts to defend against what he described as threats to “scientific independence” and to EcoHealth, whose N.I.H. grant was canceled under orders from the first Trump administration in 2020.

Dr. Morens said in the emails that he wanted to protect scientists from attacks he suggested were analogous to the “Salem Witch Trials.”

It is not clear precisely when federal prosecutors in Maryland began investigating those emails, but documents reviewed by The Times indicate it was toward the end of Mr. Biden’s presidency.

By last fall, federal prosecutors were also examining Dr. Fauci and a half dozen other scientists, including Richard J. Roberts, a Nobel laureate in medicine, who had organized a letter denouncing the cancellation of EcoHealth’s grant, according to a September 2025 subpoena to EcoHealth.

The subpoena asked for evidence that Dr. Fauci had drafted or edited documents for EcoHealth, including records related to the Wuhan lab; correspondence from Dr. Fauci’s personal email address to EcoHealth; and evidence that the group had compensated Dr. Fauci with “currency, gift baskets, wine, meals, travel” or other items.

In addition to violations of federal records laws, Dr. Morens pleaded guilty last month to receiving illegal gratuities in the form of two bottles of The Prisoner Red Blend Napa Valley wine from Peter Daszak, EcoHealth’s former president.

Dr. Daszak said the wine had been a joking allusion to calls in 2020 for his and other scientists’ imprisonment. (Dr. Daszak and EcoHealth were later barred from federal contracts under Mr. Biden over allegations that they failed to properly monitor work at the Wuhan lab, which Dr. Daszak and the nonprofit denied.)

Government oversight experts criticized Dr. Morens’s efforts to hide critical correspondence about the pandemic, while noting that Trump administration officials have themselves been said to conduct government business on private email accounts or an encrypted messaging app .

“It seems like retaliation,” Dr. Daszak said of the case, in which he appears as an unnamed co-conspirator. “The right learned pretty early on in Covid that public health is a threat to power.”

Peter Daszak, former president of EcoHealth Alliance, testified before a House committee in 2024.Credit...Ting Shen for The New York Times

Timothy Belevetz, Dr. Morens’s lawyer, said that Dr. Morens “has taken responsibility for what he did and will continue to do so.”

Dr. Fauci has said he “knew nothing” about Dr. Morens’s actions. His lawyers have created a defense fund to respond to what they described as a “legal barrage” that followed his refusal to answer questions from a Senate panel in July, including the announcement of state investigations. The panel voted to hold him in contempt of Congress.

“Dr. Fauci has done nothing wrong, and there is no basis for any investigations, whether by Congress, the Department of Justice, or state attorneys general,” David Schertler, his lawyer, said.

“The only thing that will come from these completely unjustified efforts to besmirch Dr. Fauci’s reputation and his lifelong commitment to public health is wasted time and resources that would be better used on real issues.”

The F.B.I., which has for years asserted that the virus originated in the Wuhan lab, has also more recently examined whether Dr. Fauci pushed a group of scientists in early 2020 to downplay early suspicions that the coronavirus may not have evolved naturally.

Republicans have long suggested that Dr. Fauci pressured those scientists into changing their view as they prepared an article about the pandemic’s origin, perhaps by plying one of them with a federal grant.

Dr. Fauci and the scientists deny those claims. The grant in question was reviewed and approved for funding before Dr. Fauci discussed the origin with those scientists, he said.

The researchers said their initial suspicions were allayed instead by more extensive analyses of coronaviruses in other species, like bats and pangolins, which showed that unusual-seeming features of the novel virus were in fact present in naturally occurring viruses.

Democratic lawmakers said that the attacks on Dr. Fauci and other scientists echoed past efforts to prosecute political enemies.

“It smacks of McCarthyism because it seeks to demonize individuals for seemingly fictional and illusory reasons,” said Senator Richard Blumenthal, Democrat of Connecticut. “The question is whether there’s selective targeting.”

Mr. Bongino has blamed Dr. Fauci and other government scientists not only for financing research that led to the pandemic, but also for using the resulting catastrophe to push social distancing measures and mail-in balloting provisions in 2020.

He has said on his podcast that those measures were introduced for one purpose, referring to an outcome that multiple investigations have established did not happen: “Steal the election. One hundred percent.”


reports on health and medicine. He was previously a U.K. correspondent in London and a police reporter in New York.

Glenn Thrush covers the Department of Justice for The Times and has also written about gun violence, civil rights and conditions in the country’s jails and prisons.

Kirsten Noyes and Georgia Gee contributed research. Adam Goldman contributed reporting.

Subscribe to the New York Times

Kirsten Noyes and Georgia Gee contributed research. Adam Goldman contributed reporting.

Vaccine Recommendations and Access, Plus Measles Deaths: 3 Questions, Zero Consensus
Izzy BrandstetterFigueroa, PhD, Riley Mulholland, MPH, Jess Steier, DrPH
Unbiased Science
September 3, 2026

Pixel 11 review: Google sets the bar for standard flagship phones

Guardian
www.theguardian.com
2026-09-07 02:00:26
Longer battery life, faster chip, actually useful AI tools and better cameras keep quality Android ahead of competition Google’s Pixel 11 continues to set the standard for what you should expect from the base model of a flagship phone with class-leading cameras, long software support and almost all ...
Original Article

G oogle’s Pixel 11 continues to set the standard for what you should expect from the base model of a flagship phone with class-leading cameras, long software support and almost all the bells and whistles of its most expensive phones.

The regular Pixel 11 costs £879 (€999/$899/A$1,499) making it £80 or equivalent more expensive than last year’s model as the cost of RAMageddon continues to bite . It’s not cheap by any stretch of the imagination, but it comes with 256GB of storage and is £200 less than the Pixel 11 Pro, matching rival Samsung’s Galaxy S26 .

At a glance the Pixel 11 looks very similar to the previous two Google phone iterations : glass front and back with flat aluminium sides and large pill-shaped bar at the back, which is much slimmer this time. Stick it in a case and the protruding camera all but disappears.

The camera bar of the Google Pixel 11.
The Pixel 11’s camera bar depth has been reduced by 40% compared with previous models for a sleeker look that doesn’t catch on your pocket quite as much. Photograph: Samuel Gibbs/The Guardian

The 6.3in OLED screen is crisp and super bright with up to a 120Hz refresh rate to keep your scrolling smooth. The in-screen fingerprint scanner is fast and accurate, while the camera-based face recognition works well during the day and is secure enough for use with banking apps, unlike similar systems on most Android rivals.

Specifications

  • Screen: 6.3in 120Hz FHD+ OLED (422ppi)

  • Processor: Google Tensor G6

  • RAM: 12GB

  • Storage: 256 or 512GB

  • Operating system: Android 17

  • Camera: 48MP+ 13MP UW + 10.8MP 5x tele; 10.5MP selfie

  • Connectivity: 5G, eSIM, wifi 6E, UWB, NFC, Bluetooth 6 and GNSS

  • Water resistance: IP68 (1.5m for 30 minutes)

  • Dimensions: 152.8 x 72.0 x 8.6mm

  • Weight: 197g

Faster and more efficient Tensor G6 chip

The USB-C port of the Google Pixel 11.
The Pixel 11 takes less than 90 minutes to fully charge using a 30W or greater power adaptor (not included) hitting 53% in half an hour. It also supports 25W Qi2.2 wireless charging. Photograph: Samuel Gibbs/The Guardian

The Pixel has a new generation of Google’s chip, the Tensor G6, which is up to 20% faster in tests. It significantly lags top-end rivals from Qualcomm and Apple, so won’t win any raw performance awards, but feels snappy in daily tasks and can handle mid-level gaming just fine.

The battery life is also improved over previous models, lasting about 54 hours of light use between charges. That includes actively using the screen for more than seven hours for browsing, messaging, apps and photos across a mix of 5G and wifi.

Gaming and more demanding use hits the battery harder, but the Pixel should be able to see out even the heaviest of use days.

Android 17 with more proactive AI

Screenshots from a Google Pixel 11 series showing Magic Cue’s proactive assistance in the messages app.
Google’s proactive AI tools pop up with timely information in chats and other situations to save you manually having to check your calendar, search for a restaurant or schedule an event. Photograph: Samuel Gibbs/The Guardian

The Pixel 11 runs the latest Android 17 out of the box with operating system and security updates provided until August 2033 .

The software is slick, nicely animated and well optimised, with plenty of colourful customisation if you want it. As with previous Pixels, it is a showcase for some of Google’s most advanced AI systems, many of which run on-device, making them fast.

The most interesting of the tools remains Google’s proactive AI systems, which vary a bit depending on where you live. In the UK the expanded Magic Cue from last year pops up in messages and a few other apps with information such as your schedule, flight or order details, and suggestions for good local restaurants from Google maps or photos to share in a chat. In the US, Canada, Australia and a few other regions, the feature is called “Gemini Proactive Assistance” but has similar powers. If you use a supported app, such as Google Messages, the suggestions are low-key great. Unfortunately it doesn’t work in WhatsApp, Signal or other third-party apps I’ve tried, which limits its usefulness.

In the UK and US, Gemini can go further, proactively taking action for you, such as booking a restaurant via OpenTable , which is the type of assistance that has long been promised by various AI agents.

Another useful AI feature is built directly into Google’s keyboard called Rambler, which is voice typing on steroids. You can speak naturally to it and the system will remove your ums and ers and other deviations to produce a succinct, fully formed sentence or paragraph. It makes trying to dictate a rushed messaged while hustling for public transport a lot easier, although it doesn’t show your text until after you hit the stop button, unlike most other dictation tools.

Camera

The camera app on a Google Pixel 11 shooting a garden scene.
The camera app is fairly simple to use while being feature rich with plenty of tools and modes to make the best of your photography. Photograph: Samuel Gibbs/The Guardian

The Pixel 11 has three cameras on the back and a solid 10.5-megapixel selfie camera in the screen.

The improved main 48MP camera captures excellent photos across a range of lighting conditions, including low light where its larger sensor helps capture more of what’s available. The 13MP ultra-wide camera is one of the best on a phone of this class, helpful for fitting more in the shot or providing a different perspective. But it is the 10.8MP 5x telephoto camera that sets it apart from the competition, capable of meaningfully closing the distance to objects. In bright light it can digitally zoom to 30x magnification with surprisingly good results.

Entirely new for the Pixel camera this year is the “Looks” feature , which allows you to customise how the phone captures and processes your photos for very different styles far beyond simple filters applied after the fact.

There are three versions of the default look available – natural, vanilla and shadows – plus six more-distinctive looks, including “digi” that replicates the look of the old digital cameras that has recently become popular. Each look can be customised in colour, tone, grain and sharpness, so you can get arty or replicate a particular film style.

If you are a Pixel user who has ever thought its photos look a little too cold, dramatic or overly processed, this is for you.

The Pixel shoots great video at up to 4K at 60 frames a second. The new Magic Capture feature is a one-button shooting mode that records video and photos automatically that is best used during events such as birthday parties where you want to be more in the moment but still capture the scene.

Google has also added a “ Creator Suite ” mode to the video capture, which shoots straight to a folder you can quickly storyboard and trim to create a video. It also has a teleprompter mode for shooting face to camera, a social media grid to get the alignment correct and speech enhancement that cuts out background noise.

All these features are in addition to Google’s popular Auto Best Take, Add Me and Camera Coach from last year , which makes the new Pixel camera one of the most feature rich available.

Sustainability

The back of the Google Pixel 11.
The Pixel 11 is available in a range of colours including the standard mat black (obsidian). Photograph: Samuel Gibbs/The Guardian

The battery is rated to last in excess of 1,000 full charge cycles with at least 80% of its original capacity. The phone is repairable by Google, third-party shops or self-repair with manuals and parts available .

The Pixel 11 is contains 33% recycled materials by weight. The company breaks down the phone’s environmental impact in its report and will recycle old devices for free.

Price

The Google Pixel 11 costs from £879 (€999/ $899 / A$1,499 ) in a choice of four colours.

For comparison, the Pixel 10a costs £499 , the Pixel 11 Pro costs £1,079 , the Pixel 11 Pro XL costs £1,279 , the Pixel 11 Pro Fold costs from £1,799 , the Samsung Galaxy S26 costs £879 and the Apple iPhone 17 costs £799 .

Verdict

The Pixel 11 isn’t the most advanced Google phone, nor is it the cheapest. But this middle-ground model offers more than enough to beat the competition and question whether you really need to spend more on a Pro phone.

It offers, yet again, a very high-quality experience, great triple camera, long software support and very good battery life. It won’t win any raw performance awards, so those looking for a gaming phone should look elsewhere, but it feels fast and responsive in daily use.

Google’s AI tools are second to none, particularly its Rambler dictation feature in the keyboard and the proactive Gemini features, which offer genuinely useful help even if they are not available in every third-party app.

The new camera looks feature provides more control over photos than ever before on a Pixel, so you can tailor it to your preferences or get creative far beyond simple filters.

It may not have changed all that much from last year’s equally excellent Pixel 10 , and it isn’t quite the bargain the Pixel 10a is, but the Pixel 11 is the best phone you can get under £900.

Pros: seven years of software updates, great camera with 5x telephoto, great screen, impressive local AI features, Qi2.2 wireless charging and magnetic accessory support, long battery life, good size, fast fingerprint and face recognition, 256GB starting storage.

Cons: face unlock option not as secure as Face ID, raw performance short of rivals, no physical SIM card slot in the US, heavier than rivals, not a big upgrade on previous model, no wifi 7.

The fingerprint scanner in the screen of the Google Pixel 11.
The combination of fast face recognition and in-screen fingerprint scanner with the slick screen keep the Pixel 11 feeling responsive in use. Photograph: Samuel Gibbs/The Guardian

Switzerland's Federal Government Is Replacing Microsoft on 3k Computers

Hacker News
itsfoss.com
2026-09-07 01:33:25
Comments...

Nvidia's Jensen Huang says 'AGI has arrived' and congratulates OpenAI

Hacker News
www.businessinsider.com
2026-09-07 01:23:31
Comments...
Original Article

Nvidia CEO Jensen Huang is pictured.

Nvidia CEO Jensen Huang said Sunday that "AGI has arrived" and credited OpenAI's latest model, Astra. Matt RAMEY / AFP via Getty Images

Nvidia CEO Jensen Huang tipped his hat to OpenAI on Sunday.

The chipmaking mogul took to X to congratulate OpenAI's team on the release of the company's newest and most powerful model , Astra, and made the notable statement that "AGI has arrived," a nebulous term that typically describes AI models that match or surpass human intelligence.

He also pointed out that Astra was trained on Nvidia's chips.

"From ChatGPT to o1 to Astra in 4 years," Huang said in a post on X. "AGI has arrived. Congratulations @OpenAI team."

OpenAI unveiled Astra on Thursday. The ChatGPT maker called it the world's "most intelligent and aligned model," and said it is capable of performing "the most demanding professional work with unmatched speed, accuracy, and judgment."

Greg Brockman, the company's president, said on a call with reporters on Thursday: "Welcome to the AGI era."

AGI stands for artificial general intelligence , a fuzzy concept that nonetheless has become a core goal for leading frontier AI labs.

OpenAI defines it as "highly autonomous systems that outperform humans at most economically valuable work." The company pitched Astra as a major research breakthrough and a shift in what's possible to delegate to AI. It's rolling out to customers this week.

On the Thursday call, Brockman said that in the future, he thinks people will look back and think AGI was created "about this time, and I think it might be about this model."

"For me personally, I do think we're there," he said.

Not everyone agrees.

Gary Marcus, a prominent AI researcher and critic, said Huang had jumped the gun.

"Huang gave no evidence and no definitions, which feels to me like an effort at a takeover of a scientific question by corporate fiat," Marcus wrote on his Substack Sunday evening. "Declaring victory without a definition simply muddies the waters."

Marcus attached his own 10-point definition of AGI, noting that Astra only meets one or two of those benchmarks.

"By conventional definitions, Astra still falls short," he said.

Altman, for his part, told the "Sources" podcast in a recent appearance that AGI is, at best, "a very poorly defined term. I was going to say it's like an irrelevant marketing term."

Frontier AI companies like OpenAI, Meta, Anthropic, and Google have grown increasingly reliant on advanced chips manufactured by Nvidia. In a funding announcement in March, OpenAI called Nvidia "the foundation of our infrastructure."

"Our training fleet and the majority of our inference stack continue to run on Nvidia GPUs," the company said.

That demand has translated into enormous sales for Nvidia. The company reported $96.2 billion in quarterly revenue in August, more than double the amount reported in the same period a year earlier. Its data center business, which includes its AI chips , generated $89 billion in revenue.

And in his X post on Sunday, Huang suggested there's plenty more computing power on the way.

"400K GPUs coming online next," he said, referring to graphics processing units, the advanced chips used to train and run AI models.

Read next

Truman Dickerson is the Weekend News Fellow at Business Insider, based in New York City. He covers trending tech and business news. He previously reported for The Boston Globe's Express Desk. He graduated from Boston University, where he served as editor in chief of The Daily Free Press, BU's student-run newspaper.Contact him at Tdickerson@insider.com

No Wonder ‘Coyote vs. Acme’ Is Resonating. It’s About Runaway Corporate Power

Portside
portside.org
2026-09-07 01:13:02
No Wonder ‘Coyote vs. Acme’ Is Resonating. It’s About Runaway Corporate Power Ira Mon, 09/07/2026 - 01:13 ...
Original Article

Paige (Lana Condor) and Kevin Avery (Will Forte) represent Wile E. in his Coyote vs. Acme lawsuit. | Warner Brothers

Heard on Fresh Air

Like most of you, I grew up watching cartoons. For me, the best — and purest — series was always the Road Runner. Its true hero is not, of course, that smug, beep-beeping bird. It's his perpetually thwarted pursuer Wile E. Coyote, who, alone in the most stripped-down of deserts, endures the endless futility of a Beckett character. (In fact, the character was created at the same time as Waiting for Godot .)

Scheming yet inept, Wile E.'s so hellbent on catching the Road Runner that he keeps buying shoddy contraptions — rocket skates, self-guiding bombs, earthquake pills — from a corporation called Acme. Their misfires inevitably blow him up, clobber him with anvils or send him plummeting off a cliff.

The unreliability of these products inspired a witty 1990 New Yorker piece by Ian Frazier , "Coyote v. Acme," in which Wile E. Coyote sues Acme for damages. In turn, that piece inspired the terrifically entertaining new movie Coyote vs. Acme , which has finally turned up in theaters after Warner Bros., in a controversial, Acme-esque move, tried to shelve the finished film in order to claim a tax write-off.

Blending live action with animation, Coyote vs. Acme takes Frazier's piece and transforms it into a spoofing riff on the classic legal thriller — think The Verdic t or Erin Brockovich — but one steeped in the surreal anarchy of Looney Tunes.

The story begins when the fed-up Wile E. decides to take legal action and hires an Albuquerque lawyer, Kevin Avery, played by Will Forte . Alas, the scruffy Avery proves to be a hangdog soul who fears failure so much that he only takes on small cases and settles them for a pittance.

Luckily, Avery's intern, Paige (Lana Condor), is an idealist. She pushes him into filing a massive suit against Acme, the world's most powerful company. Acme's run by the strutting toon Foghorn Leghorn and represented by killer lawyer Buddy Crane (that's John Cena ) whose body seems to be bursting from his suit.

Soon, Avery, Paige and Wile E. go out looking for evidence and encounter numerous other Warner Bros. toons: Daffy Duck, Tweety Bird, pantsless Porky Pig, and, of course, the incomparable Bugs. Along the way, Team Coyote unearths a sinister Acme project named after Sisyphus, the mythic stone-pusher whose story anticipates the coyote's own futility. Over and over, Avery wants to throw in the towel. Of course, one of the pleasures of Warner Bros. cartoons was their imaginative delight in shattering the laws of nature, often violently. Wile E. Coyote can get smooshed by a steamroller, pick himself up, dust himself off and inflate himself back to normal size. Coyote vs. Acme serves up a slew of delirious gags like this, several of them terrific, in which the flesh and blood characters face the same crazy universe as the toons.

Still, what works in a six-minute cartoon is exhausting in a feature film, which is why writer Samy Burch and director Dave Green do something the old Road Runner cartoons never did: They give their story a plot and a meaning and even some feeling.

We watch Avery develop a spine. We see Wile E.'s wistful longing beneath his Ahab-like obsession with the Road Runner. And we recognize the existential bond between the coyote and the roadrunner, the chaser and chasee, whose lives depend for their meaning on the presence of the other.

And needless to say, Coyote vs. Acme delivers a message about runaway corporate power, which may explain why viewers are enjoying it so much. Acme is the very archetype of today's mega-corporations that outsource jobs to cheaper countries, manufacture goods designed to last only until their warranties lapse, force you to wait on hold to talk to an AI bot and find it cost-efficient to pay off occasional lawsuits like Wile E.'s rather than change corporate practices. When the movie suggests that the courts and the Congress bow down to Acme's power, it's just saying what everybody knows.

Now, what was always great about old Warner Bros. cartoons was their gleeful irresponsibility. They were never sentimental or improving like Disney. And in this respect, anyway, Coyote vs. Acme violates the tradition. The movie celebrates Wile E. Coyote's willingness to fight on, even in the face of constant failure. I don't know about you, but I have to love any movie that's clever enough to turn Wile E. Coyote into a role model.

NPR does not offer or accept money for coverage or interviews.
Anyone who makes such an offer or request is not an NPR employee and does not represent NPR.


John Powers is the pop culture and critic-at-large on NPR's Fresh Air with Terry Gross. He previously served for six years as the film critic.

Powers spent the last 25 years as a critic and columnist, first for LA Weekly, then Vogue. His work has appeared in numerous publications, including Harper's BAZAAR, The Nation, Gourmet, The Washington Post, and The New York Times.

A former professor at Georgetown University, Powers is the author of Sore Winners, a study of American culture during President George W. Bush's administration. His latest book, WKW: The Cinema of Wong Kar Wai (co-written with Wong Kar Wai), is an April 2016 release by Rizzoli.

Media for the people, by the people

A free press doesn't just happen. It's something we, the public, must protect. Now is the time to give — for yourself and for the millions who rely on this freely accessible public service.

How it works

  • Donate to a Local Station

    An independent non-profit organization in our network receives your gift directly.

  • Donate to the NPR Network

    Your gift to NPR will be used to strengthen NPR and more than 240 stations across the United States.

Support the NPR Network

Posting about online presentations

Lobsters
lobste.rs
2026-09-07 01:04:28
I organize online presentations about various topics in computing. (e.g. Python, Rust, Go, Databases, DevOps etc.) I also monitor many other channels that have online "meetups". Even if they don't use Meetup to organize them. I am new here and I would like to get the opinion of the community here. ...
Original Article

I organize online presentations about various topics in computing. (e.g. Python, Rust, Go, Databases, DevOps etc.) I also monitor many other channels that have online "meetups". Even if they don't use Meetup to organize them.

I am new here and I would like to get the opinion of the community here.

  • Would it be interesting and acceptable to post such events?
  • Would it be considered self promotion if I post about events that I organize, but that features other people as speakers?
  • Should these post be one post per event or lists of events. eg. "Online Python events in the next 10 days"?

The South at a Demographic Tipping Point

Portside
portside.org
2026-09-07 00:37:28
The South at a Demographic Tipping Point Ira Mon, 09/07/2026 - 00:37 ...
Original Article

New data from the Census Bureau shows a South at the edge of extraordinary demographic shifts, including sharply falling numbers of people moving to the region and a white population that is beginning to shrink in absolute terms.

Since the end of segregation in the 1960s, the South has powered the population growth of the United States, becoming the nation’s largest region and gaining 31 congressional seats in the process. The Covid-19 era saw especially large population gains in many southern states as people moved around the country at an unprecedented rate, leaving states like California and New York for perceived greener pastures elsewhere.

But census data offers signs that this multi-decade period of continuous southern growth may be slowing significantly or perhaps even moving toward an eventual end, as rates of both domestic migration and immigration fall sharply.

At the same time, the data shows another big change in the offing as the white population of the South is aging and beginning to decrease, making people of color the drivers of the entirety of the region’s growth.

If these emerging trends continue to hold, the impact for both the region and the country will be profound.

A Slowing and Potentially Fading Southern Population Boom

The South is still by far the country’s fastest growing region, but many of its longstanding growth patterns show signs of changing. This especially true for the boom states of Florida, Georgia, North Carolina, South Carolina, and Texas that have seen uninterrupted, multi-decade population growth since the 1960s and long been the principal drivers of the region’s rapid population growth.

Between 2020 and 2025, these five southern states accounted for 83 percent of the South’s population growth and a remarkable three-fifths of the growth of the entire United States.

But the most recent census data reveals that the pace of growth in the five southern boom states subsiding — the states are still growing, just more slowly. Between July 2024 and July 2025, they were responsible for just over half the nation’s population gains.


The most pronounced slowdowns were in Florida and Texas, which have long been the country’s two fastest growing states. However, the most recent census data show Florida’s population growth falling by two-thirds from highs earlier in the decade. Texas’s growth in that same period likewise fell by more than a third. Georgia, North Carolina, and South Carolina also each saw growth slow in the period by between a fifth to just over a quarter.

Two main factors are driving this change: slowing rates of domestic in-migration and, since January 2025, the Trump administration’s aggressive implementation of a range of hardline immigration policies.

These changes hit Florida especially hard. For decades, the Sunshine State managed to be one of the consistently fastest growing states in the country both by attracting people relocating from other parts of the United States and large numbers of immigrants from outside the United States, principally Latin America and the Caribbean. (As a state with an older population, Florida derives little to no growth from births.)

Both these two sources of growth are now under stress.

In the last several years, the number of people moving to Florida from other states has fallen precipitously as housing and associated living costs have risen dramatically. Florida gained 310,892 people as a result of domestic migration between 2021 and 2022. But that number fell to just 22,517 in the one-year period ending July 1, 2025 — a drop of more than 90 percent.

At the same time, the number of immigrants arriving in Florida has also fallen rapidly. Nearly 60 percent of Florida’s population growth this decade has come from immigration. Between 2022 and 2023, Florida received 333,449 immigrants, more than any other state. But with changes to the country’s policies, that number fell by nearly half in the one-year period ending July 1, 2025.

This immigration decline, moreover, may be just a harbinger because data released this year only covers the first six months of the second Trump administration. Most experts think the rate of immigration will fall further when updated data is released in January — and remain low for the foreseeable future — as the full effects of the Trump administration’s tightened immigration policies come to bear. Some experts even think the rate of immigration could become negative for the immediate future, with more people leaving the United States for the first time in the nation’s modern history.

These changes have meant Florida’s growth rate fell from 2.6 percent between 2021 and 2022, the fastest in the nation, to a 0.8 percent between 2024 and 2025, barely faster than middle-of-pack growers like Indiana and Oklahoma.

Texas offers a slight variation on the same theme.

Domestic in-migration and immigration from outside the United States have been key drivers of Texas’s population growth, and both have fallen from their heady highs earlier in the decade.

However, Texas also has the country’s second youngest population — only Utah has a lower median age — and women of childbearing age in Texas also have more children than women elsewhere. Thus, unlike Florida, Texas also derives a sizeable share of its growth from natural population increase, or the number of births in excess of deaths. Since natural increase is less prone to fluctuation than either domestic in-migration or immigration, Texas’s growth rate has shown less volatility than Florida’s.

Still, both domestic migration and immigration to Texas are each down by more than half from their post-2020 peaks. This is enough of a reduction to mean that there will be nearly a million fewer Texans in 2030 than if growth had continued at the faster pace of the first part of the decade. The actual difference is likely to be even greater as immigration, in particular, seems likely to fall even further.

By contrast, the other three southern boom states — Georgia, North Carolina, and South Carolina — have seen growth rates slow but at a less steep rate than Florida and Texas. Each has a unique story.

In South Carolina, 81 percent of the state’s growth this decade has come from domestic in-migration as the Palmetto State has become a new retirement hotspot , increasingly filling the role long played by Florida. While the rate of domestic in-migration has varied somewhat from year to year this decade, in general it has been more constant than either Florida or Texas, steadily giving the state between 66,000 and 83,000 new residents a typical year.

The rest of South Carolina’s growth comes from immigration, a volatile source of growth there as elsewhere. However, because South Carolina derives less than a fifth of its growth from immigration — compared with nearly 60 percent in Florida and more 40 percent in Texas — those fluctuations have had less impact on the state’s overall growth rate. With one of the country’s older populations, South Carolina, like Florida, now typically sees more deaths than births each year and thus has no growth attributable to natural increase this decade.

Similarly, more than three-fifths of North Carolina’s growth this decade is from domestic in-migration, and unlike Florida or Texas, its domestic migration has remained steady.

Another 31 percent of North Carolina’s population growth this decade has come from immigration. However, perhaps because North Carolina has not been as much of a center-of-gravity for new immigrants as states like Florida and Texas, its immigrant numbers have also thus far remained more stable.

By contrast, only around 6 percent of North Carolina population growth this decade has come from natural increase compared with 28 percent in more youthful Texas.

Of these three other southern boom states, Georgia most resembles Florida and Texas. This decade, 40 percent of Georgia’s population growth has come from immigration, on par with Texas though less than Florida. Another 40 percent of Georgia’s population growth this decade has come from domestic in-migration, with the remaining 20 percent attributable to natural increase.

Like Florida and Texas, Georgia has seen its domestic in-migration numbers fall sharply as the decade has progressed. Between 2021 and 2022, more than 79,000 people moved to Georgia from other parts of the United States. However, that number fell by nearly two-thirds to just 27,333 people in the one-year period ending July 1, 2025. Immigration to Georgia also fell by around half after a surge between 2023 and 2024. But Georgia, like Texas, has a comparatively young population and has seen its growth rate propped up by a stable rate of natural increase.

However, while these three states have not seen as pronounced a falloff in population growth as Florida or Texas, this may not last, particularly as levels of immigration look likely to continue to fall in coming years.

In sum, the South used to look like a major exception to the trend of slowing American growth rates. Now even that region’s dynamic boom states are increasingly looking more and more like the rest of the United States.

The South’s White Population Growth Starting to Shrink

At the same time that overall population growth is starting to slow across the South, another demographic trend is beginning to reshape the South. New census data released this summer shows that people of color are responsible for all the region’s population growth.

Indeed, this summer’s data release reveals that the white population fell in absolute terms in 10 of 15 states in the South. These include both big states like Florida, Georgia, and Texas, as well as smaller ones like Louisiana and Mississippi.

This is a striking change from earlier in the decade when the South was one of only two regions, along with the Mountain West, where the white population was still growing.

Moreover, even in the five southern states where the white population is still growing, the rate of growth in each dropping. The trend line suggests that several — and perhaps all — of those states also soon could see declining white populations.

To be sure, the white population declines in the South are so far relatively modest compared to other parts of the country. States like California and New York have seen steeper declines. But, in this area, too, the signs are that the South is becoming increasingly more like the rest of the United States.

A Coming Southern Reapportionment Bonanza

Powered by growth among communities of color, the South is on track to gain a record nine congressional seats after the 2030 census, even with slowing population growth. This would give the South nearly 40 percent of the seats in the U.S. House. If projections hold, Texas would have just six fewer seats than California.

The question, of course, is whether the southern communities of color who are responsible for those gains in the region’s political power will see increased representational opportunities for their communities without congressional action to restore and strengthen voting rights laws.

After the Supreme Court gutted key provisions of the Voting Rights earlier this year in Louisiana v. Callais , states across the South rushed to dismantle districts where Black and Latino voters had successfully elected their preferred candidates to office, in some cases for decades. Southern lawmakers in some states even cancelled elections already in progress to give themselves time to put in place new racially discriminatory maps.

The wide expectation is that more racially discriminatory redrawing of existing maps will take place across the region after the 2026 midterms.

Key Texas officials have already signaled plans for the legislature to redrawn not only the state’s congressional map next year but also legislative and state board of education maps. A redraw of Georgia’s congressional and legislative maps could also come as soon as the legislature’s lame- duck session later this year. Tennessee lawmakers, likewise, are expected to redraw legislative maps next year to eliminate several Black districts. More states are likely to do similar map redraws.

Only Congress can stop to this wholesale attack on the political power of communities of color. If lawmakers fail to act with the urgency the moment demands, communities of color in the South will never realize the increased electoral opportunities their dynamic growth is bringing to the region. Even worse, their political power will likely move backwards.


Michael Li is senior counsel in the Brennan Center’s Democracy Program, where his work focuses on redistricting, voting rights, and elections. Prior to joining the Brennan Center, Li practiced law at Baker Botts L.L.P. in Dallas for 10 years.

In addition to his election law work, Li previously served as executive director of Be One Texas, a donor alliance that oversaw strategic and targeted investments in nonprofit organizations working to increase voter participation and engagement in historically disadvantaged Black and Latino communities in Texas.

Li was the author of a widely cited blog on redistricting and election law issues that the New York Times called “indispensable.” He is a regular writer and commentator on election law issues, appearing on PBS Newshour, MSNBC, and NPR, and in print in the New York Times, Los Angeles Times, USA Today, Roll Call, Vox, National Journal, Texas Tribune, Dallas Morning News, and San Antonio Express-News, among others.

Li received an undergraduate degree in history from the University of Texas at Austin and his law degree with honors from Tulane Law School.

The Brennan Center for Justice at NYU Law is an independent, nonpartisan law and policy organization that works to reform, revitalize, and defend our country’s systems of democracy and justice.

The United States is in the midst of a great fight for the future of constitutional democracy. The Brennan Center works to build a nation that is democratic, just, and free for everyone. We are committed to the rule of law. We work to craft and advance a transformative reform agenda of solutions that aim to make American democracy work for all.

  • We’re a think tank, conducting rigorous research to identify problems and craft transformative solutions.
  • We’re an advocacy group, fighting in court and working with elected officials to advance legislation.
  • We’re a communications hub, shaping opinion by taking our message directly to the press and public.

Now, the vital safeguards and checks and balances that underpin democracy are under attack. Amid harsh assaults on immigrants and communities of color, today ’s p olitics are polarized and Congress paralyzed. We know this: The best response to an attack on democracy is to strengthen democracy.

That’s why we’ve pioneered innovative reforms including:

  • Small donor public financing programs, which would give ordinary Americans a much louder voice in political campaigns.
  • Ballot initiatives that end partisan gerrymandering.
  • Funding incentives for state policies that both shrink the prison population and bolster public safety.

In all this, we take our cue from Abraham Lincoln’s admonition at another time of constitutional debate:

“Public sentiment is everything. With public sentiment, nothing can fail; without it, nothing can succeed. Consequently, he who molds public sentiment goes deeper than he who enacts statutes or pronounces decisions. He makes statutes and decisions possible or impossible to be executed.”

Truly effective legal and policy change requires winning, first and foremost, in the court of public opinion.

Donate to the Brennan Center for Justice

Designers should not fear being replaced by AI, industry leaders say

Guardian
www.theguardian.com
2026-09-07 00:00:25
Firms are more likely to use technology as ‘the intern in the office’ than as a replacement for skilled staff Professional designers should not feel “threatened” by the rapid growth of generative AI, according to business leaders, despite fears over job losses in the sector. With design and film pro...
Original Article

Professional designers should not feel “threatened” by the rapid growth of generative AI, according to business leaders, despite fears over job losses in the sector.

With design and film production companies and manufacturers all adopting AI at an accelerating pace, industry bodies said the technology would be used to enhance the work of designers rather than replace them.

Mat Hunter, the chief executive of the Design Council – a charity which champions the industry – said that while AI was increasingly being used by designers, “they are using it to add value”.

He said: “We are aware that technology always displaces people with new ways of working. However, we believe in the imagination and inventiveness of designers to find opportunities. We believe the UK needs more professional designers, so overall employment should continue to grow.”

Deborah Dawton, chief executive of the Design Business Council, said: “What protects great designers from AI is skill, experience, empathy and deep understanding of the sectors they operate in. So, no, [workers] shouldn’t feel threatened if they operate at this level.”

Earlier this year research, conducted by The King’s Institute for Artificial Intelligence and the Policy Institute , showed 57% of the public think AI will lead to widespread unemployment. Last week, freelancers including designers told the Guardian they were picking up “soulless” work correcting AI errors.

The Design Council said on Monday that its analysis showed the sector – which covers everything from the design of household products and fashion to building and transport design – had grown by 40% between 2019 and the end of 2023, beating the economy’s 23% average rate of expansion and leapfrogging the size of the retail industry.

Research funded by the council for its triennial overview – Design Economy 2026 – showed that sectors like construction, services and manufacturing support 2.27m design jobs, and that there has been a 15% increase in employment between 2020 and 2025.

While the figures estimating the size of the industry pre-date the arrival of dominant AI tools including ChatGPT and Claude, the employment figures to the end of last year indicate that their use has done little so far to affect the demand for workers with design skills.

Hunter said design was often considered to be an add-on or nice-to-have, but was essential to industries from advanced manufacturing to digital services.

“People don’t realise design is everywhere and is about trying to make things usable,” he said.

He added that the government’s consolidation of hundreds of public sector websites into one consistent design was an example of improvements in a service that can be supported by designers.

Dawton said AI was being used as a substitute for the work done by human designers, but “rip-offs have been happening for a very long time – AI hasn’t enabled that”.

The industry went through a similar transition when companies began using computer-aided design software, displacing draughtspeople, but Dawton said modern workers could find jobs elsewhere in the industry as it expands.

She added: “The key thing to remember is that these copycats look the same but they aren’t the same. The materials, finishes, methods of manufacture, assembly, colour, etc, mean that they are very different products.”

Over the longer term, she said: “We have to adapt to any technological change that takes place around us. The difference with the design industry is that large chunks of it drive that change.

“For most in business, efficiency/optimisation is the main conversation around AI. Designers have a second conversation with AI, which is about the potential it opens up in the hands of a creator.”

Andrew Duff, head of the Society of Garden and Landscape Designers, said many people working in his industry were nervous about AI. He said design companies were more likely to use AI as “the intern in the office” than as a replacement for skilled staff.

The Design Council report said some regions had grown strongly over the last eight years, though from a low base. There was an 88% increase in income from design across all industries in Wales and a 67% rise in the north-east of England. The south-east remained the hub for design industries, accounting for £43.3bn of gross value added (GVA) – a calculation of income from economic activity – compared with just £3.1bn in the north-east from a UK total of £136.7bn.

Its contribution was larger than the UK’s retail sector, worth £114bn, and nearly twice the size of the accommodation and food service industry, worth £70bn.

Hunter said a lack of skilled workers was a bigger problem than AI after entries for design and technology GCSEs declined by 68% over the decade to 2024.

Dawton said: “We need specialist teachers and equipment in schools if we’re going to inspire the next generation of world shapers at an early age.”

Where Hatred Lies

Portside
portside.org
2026-09-06 23:58:55
Where Hatred Lies Ira Sun, 09/06/2026 - 23:58 ...
Original Article

Reviewed:
On Antisemitism: A Word in History
by Mark Mazower
Penguin Press, 333 pp., $29.00

“I know two kinds of anti-Semites,” the unnamed narrator of For Two Thousand Years (1934), by the Romanian Jewish novelist Mihail Sebastian (born Iosif Mendel Hechter), tells a man he admires when unexpectedly confronted with his anti-Jewish prejudice. “Ordinary anti-Semites—and anti-Semites with arguments.” One of the most common anti-Jewish arguments in the West between the Russian Revolution and the fall of Nazi Germany was that Jews were an inherent component of the global spread of communism—the myth that became known as “Judeo-Bolshevism.”

Such claims ranged widely across geographical borders and political persuasions. Winston Churchill, writing in the Illustrated Sunday Herald in 1920, when he was the British secretary of state for war and air, warned about “the schemes of the International Jews,” whose “sinister confederacy” could be traced “from the days of Spartacus-Weishaupt to those of Karl Marx,” making up a “world-wide conspiracy for the overthrow of civilisation.” That same year Henry Ford’s The Dearborn Independent began publishing a series of articles arguing that

in the confidence and secrecy of Jewish communication, or buried in the Yiddish dialect, or obscurely hidden in the Jewish national press, we find the proud assertion made—to their own people!—that Bolshevism is Jewish.

Such sentiments were perfectly in line with Nazi arguments that Germany, after World War I, had been “stabbed in the back” by a Jewish-led Communist plot. In 1933, at a meeting with the physicist Max Planck, Adolf Hitler pronounced, “I have nothing against the Jews. But the Jews are all Communists, and these are my enemies. My life is against them.” His minister of propaganda, Joseph Goebbels, proclaimed in a speech at a Nazi Party rally two years later:

It was the Jew who discovered Marxism. It is the Jew who for decades past has endeavored to stir up world revolutions through the medium of Marxism. It is the Jew who is today at the head of Marxism in all the countries of the world.

Over the course of the 1930s, as Mark Mazower writes in his incisive and timely study On Antisemitism , “the European continent was gradually dividing into two armed camps.” On one side were “the liberal victors” of World War I, who supported democracy, capitalism, and the new League of Nations. On the other was a group of revisionist states that sought to create a system of racist or ethnocentric dictatorships geared to address social inequality after the Great Depression, combat materialism, and, not least, ward off communism.

“Historians have devoted much effort to examining how important antisemitism actually was to the interwar and wartime European Right,” Mazower writes, noting correctly that the question has often been “more complicated than might seem likely at first glance.” (As he points out, Italian fascism initially showed little interest in the issue, and Mussolini introduced antisemitic legislation only in 1938—with devastating consequences, long denied in postwar Italy, for its ancient Jewish community.) But antisemitism, he argues, undeniably helped give the revanchist bloc at least some of its ideological cohesion. Jews represented to these revanchists everything that they opposed, be it capitalism, democracy, hedonism, or, most menacingly, Bolshevism. At that time no one would have disputed that antisemitism was a specifically right-wing and fascist hatred, intent on reversing Jewish emancipation and all that it stood for as a symbol of humanism, enlightenment, and the equality of all members of humankind.

None of this would need stating if, several decades after the end of World War II, the meaning of antisemitism in the West had not undergone an odd transformation. In the 1970s and 1980s, particularly in the US and Western Europe, ever more commentators and intellectuals started maintaining that, in Mazower’s words, “antisemitism was the same as opposition to Israel, and that it was therefore chiefly a problem of the Left rather than the Right.” To many in the American Jewish establishment, “a new form of antisemitism” seemed to have arrived on the scene, constituting, as Prime Minister Benjamin Netanyahu would later put it, “vicious efforts to demonize the Jewish state and deny the Jewish people the right to self-determination.”

Since Hamas’s attack on October 7, 2023, and the genocidal Israeli assault on Gaza that followed, the association between criticism of Israel and antisemitism has become ubiquitous. A growing range of scholars and other observers seem to agree with Mazower that even if “some anti-Zionism masks anti-Jewish sentiments, much of it does not.” But most major American Jewish organizations have stayed attached to the assumption that the main source of antisemitism in the world today is the anti-Israel left. The Anti-Defamation League (ADL) contends that “anti-Zionism is antisemitic, in intent or effect.” The American Jewish Committee (AJC) defines anti-Zionism as “inherently bigoted” and claims that, because its “end goal…is to undermine and eliminate Jewish rights,” the Boycott, Divestment, and Sanctions (BDS) movement is driven by “antisemitic motivations.”

Various factions of the political right, meanwhile, have been quick to turn these assumptions to their advantage. Some have used allegations of antisemitism as a cudgel against the left in order to, in Mazower’s words, “police the universities, end long-standing anti-discrimination and diversity initiatives, reassert ‘Western values,’ and reaffirm the role of religion in American life.” Others have conscripted anti-Zionist, pro-Palestine rhetoric into the service of a notion of “Western values” rooted explicitly in Christian nationalism, expressing suspicion and hostility toward American Jews rather than opportunistically claiming to protect them.

There can be no doubt that, as Mazower puts it, “Jews continue to be among those groups who are targeted these days for who they are.” On March 12 a man rammed his vehicle into Temple Israel in Michigan and opened fire, injuring a security guard, before he killed himself. The suspect was identified as forty-one-year-old Ayman Mohamad Ghazali, who was born in Lebanon; four of his family members had reportedly been killed in a recent Israeli air strike. This was only the most recent in a series of obviously antisemitic acts of violence targeting Jews as Jews, including the Yom Kippur attack on a synagogue in Manchester in England, the Bondi Beach shooting in Australia, and the arson at a Mississippi synagogue. Sometimes the attackers express rage at Israel’s mass killing of Palestinians; in other cases, such as the 2018 massacre at the Tree of Life Synagogue in Pittsburgh, where eleven people were murdered, the assailants have been right-wing extremists with histories of white-supremacist views.

Precisely because it remains “an ongoing problem,” Mazower writes, “anyone who takes antisemitism seriously” ought to be “dismayed by the confusion that exists around the term, not to mention the overuse that threatens to strip it of meaning.” How did it “come to seem unassailable common sense to a lot of people” today that a prejudice once central to the ideology of far-right anticommunism now resides primarily on the left? And if that common sense is wrongheaded, how might we better understand the workings of this all-too-present bigotry?

In 1985 the former Israeli attorney general and Supreme Court justice Haim Cohn attended a seminar on antisemitism convened by the president of Israel. Over the course of three days participants lamented the rise of anti-Jewish prejudice. “The only dissenting note was struck by Cohn,” Mazower reports.

He questioned how it was that the creation of a Jewish state, instead of bringing antisemitism to an end, had led it to assume new forms. What, he asked, had gone wrong? Had their very assumptions been mistaken? If so, what was Zionism, really?

The original goal of Zionism, Mazower points out, was to normalize Jewish existence by creating a Jewish political entity and liberating the Jews from seemingly eternal persecution. Support for Zionism grew in direct proportion to violence against Jews: the first wave of secular Zionist settlement in Palestine was prompted by pogroms in the Russian Empire in the 1880s; the slaughter of up to 100,000 Jews during the Russian Civil War, along with other pogroms in Eastern Europe in the early twentieth century, further strengthened the movement.

In the interwar period antisemitism became a widespread phenomenon in Europe and ultimately a fundamental ideological tenet of its most powerful country, which got within a hair’s breadth of becoming its hegemonic power and murdered roughly two thirds of the continent’s Jews. The result was a dramatic demographic shift. In 1900 more than 80 percent of the world’s Jews lived in Europe; by 1950 the Nazis and their fascist and nationalist collaborators had reduced that proportion to less than a third. By 2010 that figure had fallen still further, to 10 percent, with some 80 percent in either Israel or the United States. It was developments in those two countries, then, that shaped how antisemitism was understood and experienced in the years that followed World War II.

In those decades American Jews enjoyed a remarkable degree of integration and upward socioeconomic mobility. “Antisemitic attitudes certainly did not disappear and episodes of small-scale violence indicated its persistence,” Mazower writes, “but major discriminatory barriers to housing, employment, and college entry came down,” and starting in the early 1960s, studies showed openly antisemitic sentiments in the country steadily dwindling.

Discussions about antisemitism in the first twenty years after the war focused in large part on lowering what barriers remained. At an ADL conference on antisemitism in 1962, Mazower notes, “participants focused chiefly on right-wing extremism (which they saw as on the wane), Christian fundamentalism, and surviving patterns of social and professional discrimination.” Two subjects, he notes, “simply never came up. One was the Holocaust; the other was Israel.”

That would soon change. Over the course of the second half of the twentieth century, as the onetime AJC president Robert Rifkind put it in 1996, the Holocaust became “one of the core cultural facts of the modern world.” What Rifkind called “the centralization of the Holocaust in the imagination of modernity” started perhaps with the broadcasts of the Adolf Eichmann trial in Jerusalem in 1961 and accelerated over the following two decades with the appearance of Lucy Dawidowicz’s The War Against the Jews (1975), the broadcast of the NBC miniseries Holocaust (1978), and the initiative to establish a Holocaust museum in Washington, D.C. At this point, Mazower aptly writes,

American Jewish organizations, which had once resisted the calls for memorialization, now insisted on it; Israeli governments, which had once been loath to discuss the past, now pressed for it. And the Holocaust itself, a genocide perpetrated by Europeans upon other Europeans, had somehow become a guarantee of America’s commitment to Israel.

Maybe “most striking of all,” he goes on, “was how the memory of the Holocaust came to be combined with attachment to Israel across the increasingly diverse and secularized world of American Jewry as a marker of ethnic identity.”

A crucial inflection point in this regard was the Six-Day War of 1967, which filled many Jewish Americans with a strong sense of identification with Israel. Mazower cites a study that the sociologist Marshall Sklare conducted in a Midwestern suburb he anonymized as Lakeville, where no other event in the past decade, in Sklare’s words, had an equivalent “impact upon feelings of Jewish identity.” In Sklare’s assessment, Mazower writes,

Israel’s creation and existence had restored what he called “a sense of meaning” for American Jews after the Second World War: At least for the inhabitants of Lakeville, it could seem that “something new, clean and good was born” out of the genocide.

But if Israel was a source of pride, the prospect of its destruction, first in 1967 and then during the 1973 Arab–Israeli War, also generated profound dread in Jewish communities, deepening the sentiment that the state’s existence was essential for the diaspora. In this way an increasingly well-integrated and affluent American Jewry was adopting feelings of paranoia and vulnerability that had a history in Israel itself. As Mazower notes, early Israeli political discourse made much of the biblical vision of the Israelites as “a people that dwells alone.” It was a view taken up in the state’s first decades by the historians of the Jerusalem school, epitomized by Shmuel Ettinger, whose Hebrew-language book Modern Antisemitism (1978) presented the phenomenon as a reincarnation of the everlasting historical hatred of Jews.

The Soviet-born, British-trained scholar Robert Wistrich helped popularize this line of thinking with his 1992 account of antisemitism as “the longest hatred,” distilling a quasi-mythical view of Jewish history as a narrative of unrelenting persecution. Israel at first perceived itself as marking this narrative’s termination. But by the 1980s the country’s self-perception had darkened under the influence of an ascendant right led by Menachem Begin—who drew on his memories of interwar Polish antisemitism—and of a growing religious sensibility that refocused Zionism on the uniqueness of the Jews as a chosen yet eternally persecuted people. Rather than the solution to antisemitism, Israel came to see itself as its main target.

It is hardly surprising, then, that within the US, “antisemitism was gradually becoming more and more linked to the question of Israel,” as Mazower puts it. The publication in 1974 of The New Anti-Semitism , by the ADL leaders Arnold Forster and Benjamin Epstein, heralded the shift. Stressing “the necessity of the existence of Israel to Jewish safety and survival throughout the world,” the authors called for the “redefining of traditional notions of anti-Semitism” to include hostility to Zionism. Leftist critics of Israel, they alleged, were the primary culprits. Forster and Epstein singled out black nationalists, whom they depicted as antisemitic in part because their support for freedom fighters in Africa and their view of the Six-Day War as an “imperialistic, Zionist” undertaking led them to express sympathy for Palestinians.

This dynamic only deepened as Israel’s post-1967 settlement project in the occupied territories accelerated, with all the violence and racism—as well as Palestinian resistance—it entailed. The European left, American liberals, and many leaders of postcolonial nations took note, and in the ensuing years their denunciations of the occupation grew in force and influence. In 1975 Arab, African, and Soviet-bloc nations supported the famous UN resolution that declared Zionism “a form of racism and racial discrimination” and equated it with the South African apartheid regime. Although the resolution was repealed in 1991 as a condition for Israel’s participation in the Madrid Conference, it had a lasting impact on who came to be considered the real enemies of the Jews.

As Mazower shows, this transformation in the understanding of antisemitism was reflected in profound changes in American advocacy for Israel. Especially after the fall of communism, such organizations as the AJC and the ADL, alongside the newly established Simon Wiesenthal Center in Los Angeles and the AJC-backed UN Watch, became increasingly engaged in “monitoring” anti-Israel bias and undertaking antisemitism research. In 2004 Congress passed the Global Anti-Semitism Review Act, leading to the creation of the Office of the Special Envoy to Monitor and Combat Antisemitism within the State Department. Because the definition of antisemitism now included “vilification of Israel,” Mazower explains, “American diplomats had an obligation to act on behalf of another nation.” Putting it differently, it also meant that antisemitism—defined as “vilification of Israel”—had become anti-American.

The best tool for the elimination of the distinction between antisemitism and criticism of Israel has become the definition of antisemitism endorsed in 2016 by the International Holocaust Remembrance Alliance (IHRA), an intergovernmental organization created to promote education about the Holocaust. Of the definition’s eleven examples of potentially antisemitic actions or statements, seven refer directly to Israel. They include denying Jews the right to self-determination, applying double standards to Israel that are not demanded of other nations, accusing Israel of exaggerating the Holocaust, and drawing comparisons between Israeli policies and those of the Nazis.

Some may find these statements objectionable, but they are not in and of themselves antisemitic. Many Jews around the world have rejected Zionism for theological or ideological reasons; many supporters of Zionism have argued that Israel should be “a light unto the nations” and thus held to higher standards; and many critics of Zionism, including Israelis, have argued that Israel has politically instrumentalized the Holocaust and drawn their own comparisons between certain Israeli actions and those of the Nazis. That is why alternative definitions have been offered, such as the Jerusalem Declaration on Antisemitism and the Nexus Document, which carefully distinguish between antisemitism and criticism of Israel.

Nonetheless, in May 2024 the US House of Representatives overwhelmingly passed the Antisemitism Awareness Act, which requires the Department of Education’s Office of Civil Rights to adopt the IHRA definition. A few days later, in a speech at a remembrance ceremony for victims of the Holocaust, President Joe Biden implicitly endorsed the analogy between protests against Israel’s brutal response to the attack by Hamas on October 7 and antisemitism. In January 2025 President Donald Trump issued an executive order called “Additional Measures to Counter Anti-Semitism,” which draws on another order he signed in 2019 that requires federal agencies to consider the IHRA definition of antisemitism when enforcing Title VI of the Civil Rights Act of 1964.


Protesters on the University of Virginia campus during the Unite the Right rally, where some chanted ‘Jews will not replace us,’ ­Charlottesville, August 11, 2017. Anadolu/Getty Images

The new executive order demands that “all available and appropriate legal tools” be used “to prosecute, remove, or otherwise hold to account the perpetrators of unlawful anti-Semitic harassment and violence.” This has enabled greater federal scrutiny of universities for allegations of antisemitism on their campuses, and the threat of withholding federal funding to compel universities to adopt new policies. For all intents and purposes, allegations of antisemitism have become a new way to muzzle opinion, silence speech, and curb academic freedom. All of this has been described by Kenneth Stern, who was the lead drafter of the IHRA definition, as making it “more, not less, difficult to tackle both antisemitism and anti-Israel dogma.”

There are two obvious problems with conflating antisemitism and anti-Zionism. The first is the peril of confusing antisemitic prejudice with political opinions in response to the reality of Israeli occupation, oppression, ethnic cleansing, and growing racism. Indeed, a good deal of protest against Israel’s actions might be seen as defending the very political values that antisemites have long sought to undermine. If we are to pay any heed to the International Court of Justice’s 2024 ruling that described the occupation of the West Bank as unlawful and noted that Israel’s practices violate the prohibition against racial segregation and apartheid, then we must conclude that criticism of the brand of Zionism implemented by the Israeli government today is not only warranted but necessary for enforcing international humanitarian law. Having once meant “the hostility faced by Jews as a minority struggling for their legal rights,” Mazower argues, now antisemitism is more often an allegation “used to defend a Jewish majority state depriving the minority within it of theirs.”

The second problem with an Israel-focused understanding of antisemitism is that it leads to a faulty account of the main sources of anti-Jewish hatred, historically and in the present. Scholars and commentators who conflate anti-Jewish prejudice with anti-Zionist sentiment often focus their attention on antisemitism on the political left and in the Arab and Muslim world. One can certainly find antisemitism everywhere, among socialists and communists as well as liberals and conservatives. Neither the political left nor the Muslim world, however, has in the past century offered a form of anti-Jewish prejudice anywhere near as coherent, influential, and dangerous as that of their right-wing counterparts.

Those who try to locate antisemitism’s ideological wellsprings in the history of the left often use as a central example the history of anti-Jewish persecution in the Soviet Union. They point in particular to Stalin’s “anti-cosmopolitan” purges of 1948, which reached their peak in the Doctors’ Plot in Moscow and the Slánský trial in Prague. Stalin’s paranoia in the last years of his life indeed had all the makings of antisemitic conspiracy fantasies, often filtered through the allegation that Jews’ allegiance to Israel made them a fifth column in the Soviet Union. In these years, sincere or opportunistic antisemites, seeking power and favor, turned against their Jewish comrades, who were often still well represented among Communists. This murderous frenzy came to an end with Stalin’s death in 1953, but the prejudice itself remained endemic in the USSR and other Eastern European Communist countries.

In the post-Stalin era Jews in the Soviet Union occupied a strange position, as Yuri Slezkine writes in The Jewish Century (2004). “Overrepresented among white-collar professionals,” they were therefore “hurt by Soviet affirmative action,” which discriminated against ethnic Jews in favor of underrepresented “national minorities.” But unlike those other minorities, they lacked a national republic that could develop “its own culture-producing institutions,” an absence that contributed to “a deafening public silence about all things Jewish” and a deep suspicion toward Jewish national identity. These are among the many factors that, between 1989 and 2006, led some 1.6 million Soviet Jews to emigrate, well over half of them to Israel. But the antisemitism that persisted under these conditions, while sometimes acute, hardly bears comparison to that of Nazi Germany. One would be hard put to show that communism as an ideology promoted antisemitism: the Soviet Union issued a decree against antisemitic violence as early as 1918.

In the US and Western Europe, antisemitism has always been far more pronounced and widespread on the right than on the left. It is true that some on the far left as well as the right have supported figures like the black nationalist leader of the Nation of Islam, Louis Farrakhan, whose antisemitic statements have long been a matter of public record: he has, for instance, falsely condemned Jews for having “owned a lot of the plantations” and for being “responsible for all of this filth and degenerate behavior that Hollywood is putting out.” Yet however distasteful such expressions of bigotry may be, they are hardly representative—Farrakhan himself has always remained a marginal figure.

Perhaps the most prominent recent antisemitism scandal on the left focused on former British Labour Party leader Jeremy Corbyn, whose critics accused him of tolerating antisemitic statements. In 2020 the United Kingdom’s Equality and Human Rights Commission, which investigated antisemitism in the Labour Party, found the organization responsible for “unlawful” harassment and discrimination during Corbyn’s tenure as leader. Corbyn stated that he was “always determined to eliminate all forms of racism,” described antisemitism as “absolutely abhorrent,” and insisted that the scale of antisemitism within Labour had been “dramatically overstated for political reasons by our opponents inside and outside the party.” Labour had essentially mismanaged the familiar debate over whether criticism of Israeli policies and expressions of support for Palestinians were fueled in part by antisemitic sentiments. Notably, Corbyn was criticized both by his own supporters after Labour adopted the troubled IHRA definition of antisemitism in 2018 and by Israel supporters for adding the caveat that “this will not in any way undermine freedom of expression on Israel or the rights of Palestinians.”

Asimilar story can be told about antisemitism among Israel’s Arab and Muslim critics. Israel’s own founding generation, as Mazower notes, recognized that Arab resistance to Jewish settlement in Palestine had its roots above all in “a struggle over land.” Already in 1930 Chaim Weizmann, the president of the Zionist Organization and a future president of Israel, warned that growing nationalist feeling among Palestinians would create a generation “who, from their point of view, are as ‘Zionist’ as we are.” Nahum Goldmann, the head of the World Jewish Congress, recalled in his memoirs that in 1956 David Ben-Gurion had taken a similar view. “If I was an Arab leader,” Goldmann remembered the Israeli prime minister telling him, “I would never make terms with Israel. That is natural: we have taken their country.”

And yet in his public statements, as Mazower observes, Ben-Gurion made all the necessary allusions to Nazism and antisemitism: the previous year he had insisted that Egyptian leader Gamal Abdel Nasser’s rhetoric “is known to us from Hitler’s day” and warned against “the Nazi dogma that is being sounded anew on the banks of the Nile.” I still recall that as a child growing up in Israel in the early 1960s, when we made bonfires on the holiday of Lag B’Omer, we would burn effigies of both Nasser and Adolf Eichmann (then on trial in Jerusalem) as the greatest enemies of the Jewish people.

Mazower does not entirely dismiss the existence of antisemitic sentiments in the Arab and Muslim world. They crop up in the rhetoric of political and military elites keen to explain away their failures, as well as in the writings of Islamicist intellectuals and movements ranging from Ayatollah Khomeini to Hamas. Some of these figures have also engaged in Holocaust denial, often in an attempt to refute the justification for establishing Israel as a haven for Jews. But as Mazower sees it, this sort of rhetoric proliferated in large part owing to the establishment of Israel and the violent expulsion of much of its majority population—some 750,000 people—in what Palestinians remember as the Nakba, the Arabic word for catastrophe. That shock led some Arab leaders and intellectuals to embrace antisemitic conspiracy theories as “a means of understanding the otherwise almost inexplicable triumph of a Jewish state and—a little later—the swift rise of American power across the postwar Middle East.”

Moreover, Mazower notes, even Arab spokespeople who take pains to resist “the spread of conspiratorial antisemitism”—as the influential Syrian Palestinian diplomat Fayez Sayegh did in the 1960s—too often find themselves accused of anti-Jewish sentiments in any case because of their opposition to Zionism. “For me it remains almost the ultimate in irony,” the Iraqi Israeli historian Nissim Rejwan wrote Irving Howe in 1974, “that what Arab anti-Semites have been doing is nothing more horrible than accepting Zionism’s own definition of itself!”

There is, to be sure, an emerging tendency among anti-Zionist critics of Israel toward rhetoric that is openly anti-Jewish bigotry. It is to be found most commonly, however, not on the left but on the vanguard of American conservatism. Mazower puts the matter succinctly: “The blanket condemnation of Israel’s critics as antisemites,” he writes, “overlooks a major long-standing source of antisemitic violence around the world today, which is the racist Right.” In May 2024 a New York Times investigation concluded that over the past decade “many Republicans have helped inject into the mainstream thinly veiled anti-Jewish messages with deep historical roots,” not least of which is the ancient allegation “that a shady cabal of wealthy Jews secretly controls events and institutions contrary to the national interest of whatever country it is operating in.”

Now the chickens are coming home to roost. In December 2025 a Manhattan Institute survey of Republicans nationwide found that almost a fifth—“typically younger,” “disproportionately male,” “more racially diverse,” “more likely to be college-educated,” and “significantly more likely” to have recently become Republicans—either self-identified as racist and antisemitic or held these views without identifying as such. That same month the institute’s publication, City Journal , gathered twenty Gen Z conservatives for a focus group and asked them, among much else, what they thought of Jewish people. “They’ve got Hollywood on lock,” said one. “Don’t they own, like, a ton of the media, and, like, just kind of everything?” asked a second. “I would say a force for evil,” said a third.

I don’t see why we support Israel. I think Israel’s a very evil state. The genocide in Gaza, killing all these poor people. And the only reason we really support them is because they are the biggest donors. We have AIPAC, and these are all Jewish-run organizations.

The responses were no less startling when members of the group were asked what they thought of Hitler. “I think he was a great leader, to be honest,” one said. “I think what he was going for was terrible, but I think he showed very strong leadership values.” The same respondent who called Jews “a force for evil” was no less explicit on this point: “I support national sovereignty, and Hitler was a nationalist. He was like, we have to take Germany back for Germans. And I feel like we should do that in America.” Some of these views bear the influence of the white nationalist Internet pundit Nick Fuentes, who has amassed a social media audience of hundreds of thousands for openly racist, misogynistic rants that repeatedly return to conspiracy theories about the control of “organized Jewry” over American politics, finance, and media.

Perhaps the most politically significant spokesperson for anti-Zionism on the American right is the Christian nationalist Tucker Carlson. Since departing Fox News, Carlson has built his independent show into a media phenomenon with an estimated online audience in the tens of millions, hosting a wide range of guests, including Fuentes last October. “You cannot actually divorce Israel and the neocons and all those things that you talk about from Jewishness—ethnicity, religion, identity,” he told Carlson. American Jews, he went on, belong to “this international community—across borders, extremely organized—that is putting the interests of themselves before the interests of their home country.”

Carlson regularly insists that his Christian faith requires rejecting group-based judgments and denies the charge that his rhetoric amounts to antisemitism. Yet he often relies on less explicit but nonetheless highly suggestive language about the Israel lobby’s threat to “Western civilization.” In March, two weeks after the start of the US-Israeli war on Iran generated a wave of discourse across the political spectrum about Israel’s influence over American political affairs, Carlson was interviewed by Piers Morgan. The “people who got Donald Trump elected,” Carlson said, are

nontraditional Republican voters who voted for Trump because he promised not to keep doing the same thing. And one of the things he promised not to do again was to hand operational authority over to Benjamin Netanyahu, as other presidents have.

Trump’s collaboration with Israel in the war against Iran, Carlson stressed, is “a betrayal on the level that I don’t think people who aren’t in those groups can understand.”

Even worse, Carlson argued, that subversion of American sovereignty has clear domestic roots. Citing the Jewish, pro-Israel, right-wing commentators Mark Levin and Ben Shapiro, he went on: “We’re not going to hand control of our foreign policy over to people like that, who don’t know anything and who don’t have America’s interests at heart or even in mind.” He claimed that “people like Levin” are engaged in a “totally immoral and dangerous” project of trying to split American society “into warring tribes,” including by “their endless harping on this antisemitism question.” In the West, Carlson continued, “it’s a human right to be treated as an individual and not be held responsible for the sins of your parents.” Yet

people like Levin and Shapiro don’t recognize that, because they are not Western in their orientation. They think that rights only apply to the groups they favor. And that idea—that evil idea, which was the basis of Nazism, by the way—that is the thing that will destroy the United States.

This is a remarkable twist on an old antisemitic canard. If the Levins and Shapiros of the United States are not Western, they must be Oriental in their orientation. They must then also favor only people who belong to that “orientation,” thereby laying the groundwork for a new kind of Nazism—presumably a Nazism rooted in the “antiwhite hate” that, according to Carlson, the ADL was not long ago “openly promoting.”

Because he is a mainstay of MAGA and a potential presidential candidate (which he denies), Carlson’s words should send chills down the spines of those very same adamant Jewish supporters of Israel who accuse the country’s left-wing critics of antisemitism. He is no longer expressing a shadowy, marginal view but a growing swell of opinion, especially among the young. Some would argue that it is now even starting to infect several of the most antiestablishment parts of the left, among which Carlson’s interviews regularly make the rounds on social media. The advocacy organization Track AIPAC, for instance, has routed its criticisms of pro-Israel lobbying through nationalist rhetoric that echoes Carlson’s. “You can’t be [ sic ] claim to be America First while pledging allegiance to Israel,” the group tweeted last year under an AI-generated image of George Washington burning an Israeli flag. “Mossad Mike Lawler is paid to pledge allegiance to the genocidal Israeli regime,” it posted two months later about the New York representative Mike Lawler, sponsor of the Antisemitism Awareness Act.

We have come full circle. Because the aim of today’s surging accusations of antisemitism is not to combat the actual phenomenon but to suppress anti-Zionism or criticism of Israel, the “new right” has been handed a convenient opening. They have seized it. They now imply, and in some cases outright assert, that the Jews are the source of the rot in American society. Ironically, by presenting protest against the indefensible as malign, the self-proclaimed guardians of the Jews have done more than just warp the meaning of antisemitism. They have made it into a tool for those eager to revive the century-old hoax that, in their unequivocal support for Israel, the Jewish state’s defenders, and by extension the Jews as a whole, have stabbed their homeland—this time America—in the back.

—This is the second of two articles.

Portside moderator - The first article can be found HERE


Omer Bartov is the Dean’s Professor of Holocaust and Genocide Studies at Brown and the author, most recently, of Israel: What Went Wrong?

The New York Review of Books has established itself, in Esquire’s words, as “the premier literary-intellectual magazine in the English language.” The New York Review began during the New York publishing strike of 1963, when its founding editors, Robert Silvers and Barbara Epstein, and their friends, decided to create a new kind of magazine—one in which the most interesting and qualified minds of our time would discuss current books and issues in depth. Just as importantly, it was determined that the Review should be an independent publication; it began life as an independent editorial voice and it remains independent today.

slop lasagna

Lobsters
underreacted.leaflet.pub
2026-09-06 23:08:41
Comments...
Original Article

i think i might have first heard this from Ryan Florence. the idea is that code is often garbage, but react components are nice because they divide that garbage into boxes that are individually replaceable.

it's easy to delete a react component, to fork it, to inline it, or to rewrite it from scratch. generally you only need to reason about the code locally. as long as your component tree design (where the state lives and how it flows down) models reality well, the specifics of the code inside every box don't matter much. worst case, some monster 10 kloc component sucks, and eventually somebody rewrites it. for the rest of your codebase, it's just a collective shrug.

this is also my philosophy applied to slop code.

i think it is completely true that slop is fucking with quality everywhere. i think that's bad. but i've also been able to make incredible progress on things i wouldn't bother picking up before.

so i'm trying to find a balance.

what generally seems to work well for me is to try to create broadly sensible layers with the right constraints on them, but then allow slop within those layers. some slop is okay, but some is not:

  • i care about how data flows through the system, what's derived from what, inputs and outputs, anything that gets stored

  • i care about the final experience. noticeable bugs, bad perf, and inconsistent behavior are bad and need looking into

  • i care about misuse of fundamental abstractions. if it's doing react wrong or database wrong etc, that's bad

other than that, i don't care that much what's in the boxes. it could be overly verbose, it could be under- or over-abstracted, it could be inelegant or amenable to cleanup, it may be just kind of dodgy. but if it works well at the leaves and it is constrained at the edges , i'm just not too worried about the middle management.

if it's easy to rewrite and it doesn't hurt the user, it's fine.

Bot Detection Without JavaScript: What My Blog Measured

Hacker News
gkoreli.com
2026-09-07 01:13:22
Comments...
Original Article

On my blog, network and request-header rules moved 277 of 372 browser-User-Agent requests out of the Browsers category: 74.5% . That gives me a much more useful account of the traffic arriving at my Cloudflare Worker. It has not established how many people read the site. Over the same two complete UTC days, the remaining 95 Browser HTML observations still differ from 14 Cloudflare Web Analytics page loads . The useful result is knowing which requests the rules separate, why they separate them, and where the evidence stops.

  • Network evidence catches requests that pass the header checks. In that window, 60 cloud-classified requests carried the navigation headers the browser rule requires.
  • A reason for each classification makes the counter explainable. It also exposed mistakes: our HTML-acceptance check mishandled valid headers, and new rows were missing their network-provenance marker. Both have since been repaired.
  • Client identity and readership need different evidence. Nine stored signature verifications identify signers, including crawlers and deliberate tests. They do not count people asking an assistant to read.
  • The remaining disagreement is a measured problem. Neither a smaller Browser count nor agreement with a script counter establishes audience accuracy.

Comparing edge page views with a script counter

Comparing two counters exposed the problem with my first-party analytics : I had treated browser User-Agents as evidence of readers. The counters measure different events, so their disagreement is a starting point for investigation. It cannot, by itself, tell me which requests were automation.

The initial alarm came from this comparison, saved on September 3:

Source Page events / loads Client or visit metric
D1 browser-UA class, seven UTC days ending September 2 1,209 578 daily client identifiers
Cloudflare Web Analytics, its rolling seven-day dashboard window 113 52 visits

The 578-versus-52 difference looked like eleven times as many readers. But a daily client identifier is not a visit, the time windows were not identical, and the script dashboard included /stats . Even the more comparable 1,209-versus-113 page totals needed those qualifications. The original query record preserves the comparison as it was made.

There was stronger evidence inside the requests. On September 2, 100 of 113 daily clients loaded one page; 156 of 164 browser-UA page observations carried no referrer. Those facts alone would not establish automation. One client classified as mobile, however, fetched 31 distinct pages in the same timestamp second. My note that night was:

i am seeing daily clients as 113 for today, and it seems unbelievable to me, like which articles are they reading, where are they coming from and so on... i just published a new article and its not even coming up in the Top pages by views section... like whats going on... Like sometimes when i publish the post i wanna see for this particular post how many readers have arrived and through which sources, its impossible to figure out. But still the most bizarre is the numbers, who is all reading these articles, it seems insane, which i appreciate but I don't want to gaslight ourselves, like something is not adding up

A useful comparison needs four decisions made before calculating the ratio:

  1. Choose the same host and explicit start-inclusive, end-exclusive UTC window.
  2. Compare page events with page loads. Report daily identifiers and visits separately.
  3. Record which routes, response types, bots, owners, and test requests each system excludes. Keep sampling information with the result.
  4. Inspect the discrepant requests and test the possible collection differences.

Cloudflare documents script blockers and browser or network loss as reasons its beacon can miss page loads. Browser caching and differing eligibility also need checking against the edge counter. A script can run in an automated browser. There is no universal ratio that separates these causes. Cloudflare Web Analytics FAQ , checked September 6, 2026.

The comparison reveals questions we can investigate. The earlier version of this article's under-two validation threshold was unsupported, and I have removed it.

The request rules in the Cloudflare Worker

The Worker classifies recorded request characteristics. It can apply those rules deterministically without establishing who controlled the client. This section is for someone implementing the classifier; the measured results below can be read without the implementation detail.

The edge counter schedules a D1 observation after an eligible successful page GET . It excludes prefetches, /stats , API routes, and non-page responses. It includes HTML and negotiated Markdown page responses; direct .md requests are outside this counter. That collection boundary comes from the eligibility code .

Four sources of evidence feed the classification:

  1. Network metadata. Cloudflare supplies the client's autonomous system number (ASN). This describes the network reaching the edge. A client cannot change it by editing an HTTP header, but can reach the site through another network or proxy. It does not establish the operator's purpose.
  2. Fetch Metadata. Sec-Fetch-Mode: navigate and Sec-Fetch-Dest: document describe a page navigation. Sec-Fetch-Site can be none , same-origin , same-site , or cross-site ; the classifier records it but does not require one value. Header definitions .
  3. Accept and Accept-Language. The rule checks whether HTML is acceptable and whether a language header is present. These are request characteristics, not evidence of attention.
  4. User-Agent. Named crawler and assistant rules match the client's declaration. The same string also supplies the browser-version claim used below. A client can imitate it.

D1 stores selected headers, derived flags, names, and classification reasons. It does not retain the full User-Agent. Existing Cloudflare operational logs contain additional request detail. Storing a derived flag is useful, but does not preserve enough information to replay every future parser change.

Rule one: classify known hosting networks before browser shape. After signature and named-client rules, a browser-UA request on the curated hosting list becomes cloud-browser , even if it passes the navigation-header check. In the original 72-hour log sample ending September 3, 430 of 844 successful page GETs were navigation-shaped traffic on hosting networks. The largest cluster was 374 requests attributed to one Google Cloud client claiming Chrome Mobile 114. Header presence could not separate that cluster. Original measurement and coverage .

A cloud browser may be carrying out useful work for a person. I want that access recorded and visible. The network label describes how it reached the site, and moving it out of Browsers does not remove its observation.

The curated network list excludes several shared service and consumer-VPN networks. Broad hosting lists can include networks that carry legitimate browsing. Curating the list accepts the opposite cost: some automation will arrive through networks outside it.

Rule two: check missing Fetch Metadata against the claimed browser version. The implementation checks Chromium 76+, Firefox 90+, and Safari/iOS 16.4+. If a request claims one of those engines but lacks Sec-Fetch-Mode , it becomes http-client with reason no-fetch-metadata . Older or unreadable claims receive legacy-browser . A request with Fetch Metadata that fails the navigation combination receives not-navigation-shaped . Classifier code .

Those version thresholds follow browser support data , checked September 6. They establish an expected browser capability; they do not authenticate each request or establish a zero false-positive rate for every embedded client. We do not require Sec-Fetch-User : the Safari compatibility investigation found support for other Fetch Metadata headers without it.

The follow-up audit also found a defect in our own Accept check. The original extractRequestMetadata() returned acceptsHtml: 1 for Accept: text/html;q=0 and 0 for Accept: text/* . The first explicitly excludes HTML, while the second admits it. The substring check ignored HTTP quality and media-range rules. RFC 9110, Accept semantics ; recorded reproduction .

The September 6 repair now evaluates quality, specificity, and matching representation parameters. In twelve selected local ingestion cases, seven incorrect acceptance results became zero; this is a regression check, not a production error-rate estimate. Executed experiment and artifacts . Stored booleans cannot tell us how many historical requests the defect affected.

What open-source classifiers contributed

The useful prior art supplied specific pieces of this design: declared-bot detection, network classification, and retained reasons. Our reading did not establish that the combination is novel, or that every other counter misses the same traffic.

Project What the inspected source contributes Boundary
`isbot` User-Agent matching for clients that identify themselves Its stated scope excludes programs disguising themselves as users
Plausible UA checks, explicit ingestion outcomes, and consumption of an upstream IP classification Reading that code does not reveal or reproduce the upstream classifier
GoatCounter Server-side bot classification and retention of bot reasons Its categories and collection rules differ from ours
Anubis A combination of browser headers that adjusts a classification weight The rule file warns that automated scrapers can bypass it

These sources were checked on September 6; revisions and source boundaries are in the pinned review . isbot remains useful for the declared clients it aims to recognize. Anubis makes access-control decisions, whereas this counter aims to retain and explain access.

The part I adopted from GoatCounter was a reason attached to each classification. A lower total tells me little; a table of requests and the rules that classified them tells me what changed.

Agent names, signatures, and request purpose

The site already records named User-Agent matches and verifies supported Web Bot Auth signatures. The mistake was treating those identity signals as proof that a person asked an assistant to read. The signature machinery is useful; the grouping and wording need correction.

In the saved cohort since the verifier launched, through September 5 UTC, D1 contains nine requests with a stored verified result:

Signer Requests What this record establishes
ahrefs.com 5 The Worker recorded successful signature verification
crawler.exa.ai 2 The Worker recorded successful signature verification
assistbot.duckduckgo.com 2 Successful verifications from our deliberate DuckAssist tests

These are stored verifier outcomes, not a fresh independent verification of every original signed message. The cohort and queries separate the deliberate tests. The previous claim that no signed request had arrived was wrong.

A signed client can be a crawler: Exa documents ExaSearchBot as fetching and indexing pages. A signature associates a request with a signing identity under the verifier's checks. It does not establish the initiating person's identity, delegation, or purpose. The versioned Web Bot Auth draft makes those boundaries explicit; citing it is not a claim that our implementation conforms to every provision of that draft.

The current stats grouping puts signed-agent and ai-assistant together under AI agents. That grouping conflates authentication with client role and is pending repair. Routine ingestion matches User-Agent rules and verifies signatures; it does not perform the vendor-IP validation we did manually in the fetcher-header probe study .

The older thirty-day snapshot recorded 33 ChatGPT-User -matched observations across eight paths, alongside 51 PerplexityBot observations across 31 paths, 37 Amazonbot observations across 36, and 21 GPTBot observations across 21. Those are recorded access patterns. They do not establish 33 human reading actions, and similar request/path totals do not independently prove why a crawl happened.

The reason to keep all this traffic is the publishing question behind it:

i even want to know the headless browsers on home connections, like Meshclaws or Hermess cloud agents using playwright or cypress or any type of automation, we don't want to miscount or block them, on the contrary, I want to embrace them, anyone can read my articles... I just want full visibility and transparency and categorization, like we need to explicitly know who is who (of course with respecting PII), but lots of people might stop reading articles directly and might use their AI agents like Meshclaws and Hermes agents running on the cloud to read my articles and so on, and I want to know who is reading what, like I am trying to understand how people are using my articles... Ideally I would love to have some kind of official citation for my articles as well, like you know how Arxiv or research papers have citations? I want to embrace that as well, like people reading and finding my content valuable and recommending them to cite them as needed, and have some kind of visibility into those citations. Like another example is how scholar.google.com shows Total citations Cited by 51, something like that would love to.

For that question, a useful table distinguishes the claimed client, verified signer, documented role, and any directly known trigger. Ordinary production requests usually leave the trigger unknown. The identity study can investigate that separately without turning this counter into a count of unobserved reading actions.

What the Browsers category cannot establish

Browsers contains requests that pass our rule, including some automation. It can also exclude legitimate access through cloud browsers or unusual clients. Its error can run in both directions, so it is not an established lower or upper bound on people.

Our fetcher-header study captured Grok fetching through clients with browser User-Agents and navigation headers, including requests that passed the browser rules. An agent driving an ordinary browser can produce the same characteristics the counter uses for human browsing. The access is valuable, but headers alone need not reveal whether it followed a conversation, a scheduled job, or another process.

The new production window adds a concrete case to investigate: 22 of the 95 Browser observations share the uniuit.com referrer and a repeated homepage/article pattern. An inspected Worker log declares a Redroid Android WebView; the later zone-data correlation finds that declaration among candidates for 19 of the 22 rows. Diagnostic evidence and matching limits .

That supports investigating automation, not naming an authenticated operator. A User-Agent is a declaration, and the zone matches lack a shared request identifier. Setting all 22 observations aside as a sensitivity calculation still leaves 73 versus 14, or 5.21×. Explaining this cluster would not explain the whole discrepancy.

The subsequent repair is covered in How I Filter Referrer Spam Without Deleting Analytics History : a versioned reporting policy excludes matching observations while retaining the evidence and limiting which referrer names appear publicly. The historical comparison above keeps its original scope.

The useful claim is narrower than a reader count: these requests met a documented rule. Determining how much of that category represents people consuming the article requires evidence the rule does not collect.

How classification changes affected history

Changing a classification must preserve what was observed before the change. Our first repair briefly made the old audience disappear from the default view, and restoring that history mattered as much as improving the rule.

The evidence columns did not exist on old rows. When the first header and network checks shipped together, the default stats view showed three page views for the week.

why did we lose all the views lmao... you need to migrate properly and maintain the historical views, even if it was miscalculated doesn't matter, its okay, we can trace the commit history and know with full honesty what happened and why

The week came back, marked with the evidence available when it was recorded. The first unsuccessful name for requests that failed the new checks was Browser-like:

this is kinda confusing, and saying browser-like is a little misleading and screams low confidence, like what is the browser like, do we know deterministically or what? Browsers Browser-like Bots AI UAs All

You need to ground yourself better

Replacing that confidence label with named rules made the system easier to inspect. It did not make every classification an authenticated fact about the client.

The next mistake was accepting a broad claim that WebViews omit Fetch Metadata. We had read several defensive projects and planned to wait for our own referred traffic before deciding.

can't we learn from other people and prior art? this is 2026 september

The support history was already available. A 2019 Android WebView issue recorded the headers after Chromium 76. The September 2025 Safari compatibility report described production observations and a BrowserStack reproduction: Safari and its WebViews sent Sec-Fetch-Mode while lacking Sec-Fetch-User . That was enough to reject the blanket WebView claim. It did not remove the need to investigate unusual requests on our own site.

Then there were the logs we already had:

dont we already have some raw logs in cloudflare?

The original 72-hour sample contained 112 navigation-shaped requests outside the hosting list out of 844 successful page GETs. That is about 13% satisfying the sample's rule, not 13% proven human readership. All twelve externally referred requests in that sample carried Fetch Metadata; twelve is useful corroboration, not a false-positive-rate measurement.

For older rows, the retained zone data allowed a partial reconstruction. Of 1,962 pre-evidence observations, 1,429 received an ASN only when all sampled requests in the matching hour/path/country/device group agreed on the network. There were 191 ambiguous rows and 342 unmatched rows. The migration records reconstructed networks as asn_source = 'zone-sample' . This is a sampled attribution method, not an exact request join. Reconstruction method .

The follow-up found another defect: all 1,797 inspected observations from September 3 05:05:22 through September 6 01:29:53 UTC had null asn_source . The then-active INSERT omitted that field. My previous statement that every row records its network provenance was therefore wrong. The migration marked rows that existed when it ran; that did not fix future ingestion. Provenance audit .

The September 6 future-write repair now records asn_source = 'request' when an ASN is available, leaving missing networks unmarked. Deployment and subsequent-write check . Preserving history requires checking both the migration and subsequent writes. A classification revision, a database migration, and a Worker deployment are separate events; recording one does not establish the others.

What the September 4–5 measurements show

The new rules have a large measured effect, and the remaining count is still uncalibrated. This comparison uses September 4 00:00 through September 6 00:00 UTC, with the end excluded, for both sources.

UTC day Browser-UA observations Browsers HTML observations Non-bot RUM page loads
September 4 224 49 7
September 5 148 46 7
Total 372 95 14

The rules reclassified 277 of 372 browser-UA observations, or 74.5%. Of the cloud-classified requests in that original browser-UA population, 60 passed the navigation-header combination. Network evidence therefore separates requests the header combination alone would admit. This measures rule effects; we did not label all 277 requests independently and measure classifier accuracy.

RUM excludes bots and the two /stats loads in this window; all returned RUM groups had sample interval 1. Including its bot-tagged loads raises the comparable RUM total to 16, leaving 95/16 = 5.94×. Bot filtering alone does not account for the discrepancy. D1 applies owner exclusions, but incomplete owner marking remains a comparison limit. SQL and saved results .

A read-only extractor reproduced the 95/14 = 6.79× result in a separate capture and saves queries, sampling, and deployment metadata. The extraction command makes the comparison repeatable. It does not turn the two sources into identical populations or authenticate their results independently of Cloudflare.

The next investigation uses existing invocation logs and traces, then controlled clients to test classification, script execution, and beacon delivery separately. Individual trace-span inspection and those controlled-client trials remain open. The longer calibration is planned around September 17. This article does not need their outcome to report the rule effects already measured, but it cannot claim the reader-count problem is solved.

Applying this to another site's analytics

The transferable method is to retain useful request evidence, make classification reasons visible, and compare collection stages before interpreting the total as readership.

  1. Define the event. State which methods, routes, responses, representations, and prefetches enter the counter.
  2. Compare explicit windows and units. Page observations, script page loads, visits, and daily identifiers answer different questions. Record filters and sampling.
  3. Keep evidence beside the classification. Preserve the fields needed to inspect a rule and disclose where only derived values survive. Check that new writes populate provenance.
  4. Evaluate network and browser shape separately. A hosting list and a navigation-header check catch different traffic. Document shared-network exclusions and version handling.
  5. Separate declared identity, verification, and purpose. Preserve signed crawler access as well as assistant access. Do not manufacture a human trigger from either.
  6. Keep history explainable. Record rule changes and reconstruction methods. Preserve ambiguous and unmatched observations.
  7. Test the unresolved mechanisms. A smaller Browser total or a favorable ratio is not a completion criterion.

This leaves a more useful counter: I can inspect why requests were classified and choose the next experiment from the evidence. It leaves readership as a question to measure.

Method, limits, and revision history

The research directory preserves the original measurements, subsequent corrections, primary-source notes, and aggregate exports. The claims ledger distinguishes completed research from pending implementation. Its correction notes supersede overclaims in the dated early artifacts; those artifacts retain their historical wording.

D1 observations, private Worker logs/traces, and Cloudflare RUM are separate collections. A field absent from D1 may exist in operational diagnostics. The published evidence contains selected observations and reviewed aggregates, not access to the private Cloudflare account. Stored signature results are not independent re-verification, and inferred zone matches are not shared-ID joins. No verified count of distinct people, reading actions, or citations follows from these counters.

September 6 correction: added the September 4–5 measurements; removed the universal accuracy thresholds and lower-bound claim; corrected signature arrivals, vendor-IP validation, and network provenance; disclosed the Accept defect. The original publication date and URL remain. The parser and future-write provenance repairs were subsequently merged and deployed on September 6; the local regression result is linked above. The full browser/beacon study remains separate work.

I wanted this page to exist on the night I could not find it. If you have a captured browser request that contradicts one of these rules, or a measured explanation for a similar counter gap, tell me where this is wrong.

Show HN: Engrim – A universal, local-first SQLite memory engine for AI CLIs

Hacker News
github.com
2026-09-07 00:49:36
Comments...
Original Article

CI PyPI Python License: MIT Local & private

The Universal Cross-Model & Cross-Agent Episodic Memory Store.

A local-first, project-scoped SQLite memory engine that allows developers to freely switch between models and environments ( Google Antigravity , Claude Code , Cursor MCP , Windsurf ) on the SAME project without losing architectural decisions, user constraints, or project state.


1. The Core Value Proposition

"Why pay for 200,000 tokens of forgotten noise on every turn? The models are disposable utilities; your project's decisions are not."

As context windows scale to 1M+ tokens, developers face attention dilution : reasoning degrades, cost multiplies with every conversational turn, and clearing context causes total amnesia.

engrim replaces attention dilution with 4,000 characters of curated episodic working memory :

  • Switzerland of AI Memory : Decouples project intelligence from any single AI vendor or proprietary cloud silo. Switch from Gemini 3.8 in Antigravity to Claude 3.7 Sonnet in Claude Code to GPT-4o in Cursor mid-project — your agents pick up right where the others left off.
  • Save Button for Autonomous Coding : Externalize decisions, constraints, and state as you work. Clear your agent session freely ( /clear ) and watch context reload intact.
  • Smart, Hot Context Loading : Combines SQLite FTS5 (bm25 keyword search) with static vector embeddings ( model2vec ) in a zero-latency hybrid reciprocal-rank fusion engine.

2. Empirical Proof (The 105-Session Case Study)

Tested across 105 continuous sessions on a 50,000-line algorithmic trading system. Zero regressions across 186 unit tests, zero context amnesia across model switches.

In production testing on an active algorithmic trading codebase running real capital:

  • Over 153,000 tokens of work across days of architecture, parameter tuning, and debugging was consolidated into an active memory pack under 1,000 tokens (<1% of the context window).
  • That is a 99%+ cut in reloaded context cost on every session restart.
  • Seamlessly switched between Google Antigravity CLI, Claude Code, and Cursor MCP on identical repos with zero model drift or architectural regression.

3. Architecture

graph TD
    subgraph Agents ["Supported Agent Environments"]
        AGY["Google Antigravity<br/>(PreInvocation & Stop Hooks)"]
        CLAUDE["Claude Code<br/>(SessionStart & Stop Hooks)"]
        CURSOR["Cursor / Windsurf<br/>(Model Context Protocol stdio)"]
    end

    subgraph CoreEngine ["engrim Core Engine (v1.3.0)"]
        ADAPTERS["Adapters & Hooks<br/>(agy, claude, mcp)"]
        PROVENANCE["Agent Provenance Engine<br/>(origin_agent tracking)"]
        ROUTER["Hybrid Retrieval & Minder<br/>(bm25 lexical + vector cosine)"]
    end

    subgraph Storage ["Local-First SQLite Store (~/.engrim/memory.db)"]
        MEMORIES[("Curated Memories<br/>(decisions, facts, feedback)")]
        FTS5["FTS5 Full-Text Search<br/>(porter stemmer, triggers)"]
        VEC["Vector Embeddings<br/>(model2vec static embeddings)"]
        LOG["Flight Recorder Log<br/>(turns + action lines)"]
    end

    AGY <-->|"hook / CLI"| ADAPTERS
    CLAUDE <-->|"hook / CLI"| ADAPTERS
    CURSOR <-->|"JSON-RPC (stdio)"| ADAPTERS
    ADAPTERS --> PROVENANCE
    PROVENANCE --> ROUTER
    ROUTER --> MEMORIES
    MEMORIES --- FTS5
    MEMORIES --- VEC
    ADAPTERS --> LOG
Loading

4. Multi-Agent Quickstart

Installation

Auto-Detection (Recommended)

Run engrim setup without arguments. It automatically detects installed environments on your machine and configures them all:

  • If ~/.gemini exists $\rightarrow$ wires Antigravity lifecycle hooks, skill, and MCP server.
  • If ~/.claude exists $\rightarrow$ wires Claude Code SessionStart, Stop, status line, and CLAUDE.md.
  • If ~/.cursor exists $\rightarrow$ generates and merges Cursor MCP configuration.

Explicit Platform Setup

Google Antigravity

  • Configures ~/.gemini/config/hooks.json to execute engrim hook --agent agy --event boot on PreInvocation and engrim hook --agent agy --event stop on Stop .
  • Deploys the canonical Antigravity skill to ~/.gemini/config/skills/engrim/SKILL.md .
  • Registers the MCP server in ~/.gemini/antigravity-cli/mcp_config.json and ~/.gemini/config/mcp_config.json .

Claude Code

  • Wires SessionStart , SessionEnd , Stop , and UserPromptSubmit hooks in ~/.claude/settings.json .
  • Configures live ambient status line in Claude Code's status bar.
  • Appends memory usage notes to ~/.claude/CLAUDE.md .

Cursor

  • Adds engrim to ~/.cursor/mcp.json running engrim serve --mcp .

Windsurf

Add engrim to your ~/.codeium/windsurf/mcp_config.json :

{
  "mcpServers": {
    "engrim": {
      "command": "engrim",
      "args": ["serve", "--mcp"]
    }
  }
}

All Platforms

  • Configures every supported environment in one command.

(Use --dry-run with any setup command to inspect changes without modifying disk).


5. Agent Provenance Tracking

When multiple agents collaborate on a single codebase, provenance matters. engrim records the origin of every memory entry with the origin_agent field:

  • Allowed values: antigravity , claude-code , cursor , cli , or user .
  • Automatically populated based on the active hook, MCP client, or CLI session.
  • Subtly surfaced in engrim context and engrim list :
🧠 engrim · memory restored for this project — you don't have to re-explain · /workspace
  18 of 54 curated records loaded (~3850 chars) · the rest one `recall` away

[DECISION]
- #961 [DECISION] (via Antigravity): Inverted stop loss matrix for high volatility  (risk, execution)
- #942 [DECISION] (via Claude Code): Switched primary database from MongoDB to PostgreSQL  (db, schema)
- #910 [DECISION] (via Cursor): Standardized on Pydantic v2 schemas across API boundaries  (api, types)

Existing databases are non-destructively migrated on first access via ALTER TABLE memories ADD COLUMN origin_agent TEXT .


6. Hardened Model Context Protocol (MCP) Server

Launch the zero-dependency, JSON-RPC 2.0 stdio MCP server:

engrim serve --mcp
# or: engrim mcp

stdout is strictly reserved for JSON-RPC messages, redirecting all diagnostic logs to stderr .

Core MCP Tools Exposed:

Tool Signature Purpose
engrim_recall (query: str, project: str = "auto", k: int = 5, type: str = None) Hybrid (keyword + semantic) search over project memory.
engrim_add (type: str, summary: str, detail: str = None, tags: list[str] = []) Write a durable memory record persisted across sessions.
engrim_context (project: str = "auto", budget: int = 4000) Retrieve the session-boot memory pack within a character budget.
engrim_review (project: str = "auto") Check uncaptured decisions from transcript logs before clearing.

7. CLI Reference

Command Usage Description
engrim add engrim add -t decision -s "..." [--origin-agent agy] Insert memory record (types: decision , fact , feedback , state , user , reference ).
engrim recall engrim recall -q "database" Ranked hybrid recall for the project ( --log searches raw turns).
engrim context engrim context [-b 4000] Priority-ordered, budget-capped session-boot pack.
engrim hook engrim hook --agent agy --event boot Agent lifecycle hook runner for Antigravity and Claude Code.
engrim setup engrim setup [--agy|--claude|--cursor|--all] Universal multi-agent environment configuration.
engrim serve engrim serve --mcp Start stdio MCP server for agent integrations.
engrim review engrim review "Safe to clear" coverage check: scans logs for uncurated decisions.
engrim list engrim list [-k 20] List recent memories for the current project.
engrim supersede engrim supersede --id 12 --status superseded Mark a record superseded without erasing history.
engrim sync engrim sync [DIR] Mirror markdown memories into the store (idempotent seed-once).

8. Continue-As-Clear Workflow

  1. Capture as you work : Whenever a major decision or architectural rule is made, run engrim add or invoke engrim_add via your agent.
  2. Use resume-pointer : Before ending a session or clearing, add a record tagged resume-pointer describing the immediate next task. The newest pointer is pinned under [▶ RESUME HERE] at the top of the next session's boot pack.
  3. Verify with engrim review : Check that all recent decisions are captured.
  4. Clear freely ( /clear ) : The session window is wiped clean; engrim automatically re-injects the active memory pack on the next prompt or invocation.

9. Security & Privacy

  • 100% Local & Offline : All memory records and logs reside in a local SQLite file ( ~/.engrim/memory.db ). No telemetry, no cloud sync, no tracking.
  • Model Storage : Uses model2vec for local static embeddings (~30ms load time, no GPU required, runs on CPU). Can run pure-lexical ( ENGRIM_EMBED=off ) for zero extra dependencies.
  • POSIX File Permissions : Databases are created with restricted owner-only permissions ( 0600 ).
  • Git Protection : *.db is gitignored by default; your memories never accidentally commit to version control.

10. License

MIT © 2026 Tim Gordon.

TiVo to charge money for skipping commercials in your own recordings

Hacker News
cordcuttersnews.com
2026-09-07 00:41:01
Comments...
Original Article

TiVo has begun notifying customers of a significant change to one of its longest-standing and most popular DVR features. Starting November 2, 2026, the current version of SkipMode that allows users to jump over commercial breaks with a single button press on the remote will no longer be available. The company is instead preparing to test a new paid add-on called Premium Auto Commercial Skip.

SkipMode has been a core part of the TiVo experience for years. After a supported show finished recording, the service added markers that let viewers leap from the end of one program segment directly to the start of the next, bypassing the ads in between. On newer TiVo Experience 4 devices, users could even set the feature to automatic so commercials were skipped without any remote interaction. The feature was limited to popular prime-time programming on major networks, typically appearing a short time after a show aired.

Under the new plan, that one-button and automatic functionality will disappear from the standard service. Viewers will still be able to skip commercials the traditional way by using the 30-second skip button or the fast-forward control on their remotes. Those manual methods have existed on TiVo boxes for more than two decades and do not rely on the company’s commercial-detection data.

In November, TiVo will offer a 30-day free trial of the new Premium Auto Commercial Skip service. After the trial ends, customers who want to keep the automatic or one-button skip capability will need to add it to their accounts for an extra monthly fee. The company has not yet published the price. Additional details about how the service works, which devices and recordings it will support, and exact pricing are scheduled to be sent to customers before the November 2 cutoff.

No immediate action is required from subscribers. Existing recordings that already carry SkipMode markers will continue to function as they do today until the change takes effect. After that date, new recordings will no longer receive the free skip data.

The shift comes as TiVo’s parent company, Xperi, has moved away from manufacturing new DVR hardware and focused more on its smart-TV operating system and advertising technology. Automatic commercial skipping has long been a selling point that distinguished TiVo from cable-operator boxes and competing DVRs. Making the most convenient version of that feature an optional paid add-on represents a notable change in the company’s relationship with its remaining DVR customers.

Many longtime users have relied on SkipMode as a primary reason to keep their boxes and service subscriptions active even as live-TV viewing has declined. The new structure will force those customers to decide whether the convenience of automatic ad skipping is worth an additional monthly charge or whether they will return to manually fast-forwarding through commercials.

TiVo has said it will provide more information in the coming weeks so households can evaluate the trial and the eventual paid option before the current free feature disappears. Until then, the familiar Skip button on TiVo remotes will continue to work as it has.

Please add Cord Cutters News as a source for your Google News feed HERE . You can watch today’s top cord cutting stories on our YouTube channel HERE . Please follow us on Facebook and X for more news, tips, and reviews. Need cord cutting tech support? Join our Cord Cutting Tech Support Facebook Group for help.

I refused to train the AI that could replace me

Hacker News
restofworld.org
2026-09-07 00:38:22
Comments...
Original Article

After completing a Ph.D. earlier this year on technologies such as artificial intelligence reshaping agricultural work in South Africa, I was ready to start an academic career. I imagined that I would teach students, supervise research, and shape the next generation of scholars. But my first job offer did not come from a university. It was from a recruiter, who invited me to train an AI system to design assessments, teach undergraduate students, and mark essays. In short, I was being asked to transfer everything I had learned over the last decade to AI.

I agreed to an interview. You may ask: Why would anyone agree to train an AI model in the very skills they took years to acquire? The answer, in part, is the socioeconomic reality in South Africa. The role offered 600 rand ($37) an hour in a country where the national minimum wage is 30.23 rand ($2) an hour, and where youth unemployment stood at 47.4% in the second quarter of 2026. So when the opportunity to earn a livelihood collides with your principle on helping a technology that can compete with you one day, what do you do? I am not the only one who had to consider that trade-off.

When the opportunity to earn a livelihood collides with your principle on helping a technology that can compete with you, what do you do?”

People are increasingly being hired to train the very AI systems that will perform their jobs one day. Highly skilled writers are being paid low wages to “humanize” or edit AI-generated texts. In India, waste sorters and welders are strapping phones or cameras to their heads to capture their everyday activities for 250 rupees ($2.60) an hour. That data is being used to train humanoid robots that will replace the very jobs these workers rely on.

While a waste sorter sits at the opposite end of the pay scale from me, we are essentially doing the same job: transferring human knowledge and judgment to a machine. If this becomes a defining feature of the AI economy, Africa will be exposed . The continent has one of the lowest AI adoption rates in the world, with most countries below the global average of about 18% among the working-age population. South Africa fares slightly better, with an adoption rate of 23%.

Yet the continent has among the youngest populations, and a growing pool of highly educated professionals in an environment of widespread unemployment, low wages, and modest economic growth forecasts. For AI companies, there is an incentive to extract expert knowledge from African professionals at relatively lower costs than in Western societies.

The AI jobs on offer today for someone in Africa who is well-educated are different from the data labeling and data annotation jobs that have long been outsourced to Kenya, Nigeria, and elsewhere. The requirements are also different: I had been scouted not just to transfer knowledge but also my judgment that I have gained from years learning in the classroom, and teaching undergraduate students. Over thousands of hours, I have learned to judge what concepts truly matter in an undergraduate course, how to evaluate essays, how to tell the difference between memorization and genuine understanding, and why one student should get 75% instead of 60% on an essay. The AI would not simply learn what I knew; it would learn how I decided.

What does it mean for us to hand over the judgment that defines our character and our professions to a machine?”

Several white-collar professions are defined by discretion. Lawyers exercise legal judgment, doctors exercise clinical judgment, and teachers exercise pedagogical judgment. This kind of knowledge was considered difficult, if not impossible, to automate because it depends on interpreting context rather than simply applying rules. This is why doctors, lawyers, and engineers are being actively recruited now by AI platforms like Outlier, Mercor, and Surge.

In my own recruitment process, no human was involved. I was interviewed for 45 minutes by an AI, which then emailed me feedback on my strengths and weaknesses. Traditionally, this decision was made by humans, but now AI has taken over, and decides who gets hired. The AI that interviewed me suggested I retake part of the assessment. I didn’t, and stopped pursuing the job.

I wish I could tell you my choice was based on principle, that I refused because I feared helping to build the technology may replace the work I do. But I still don’t fully understand the source of my own discomfort, nor do I have answers to the questions I have been grappling with. What does it mean for us to hand over the judgment that defines our character and our professions to a machine? What kind of judgment are we teaching these systems? Are we teaching them to be fair, to be good, to be ethical, and to truly recognize context ?

I still do not know why I walked away from the AI training job. Perhaps I will understand better in time. Meanwhile, I continue to look for an academic job teaching students, not AI.

Coop – Isolated VM Environments for Running Claude Code and Codex

Hacker News
github.com
2026-09-07 00:18:28
Comments...
Original Article

Isolated VM environments for running Claude Code and Codex.

Pronunciation: "coop" (/kuːp/) — one syllable, rhymes with "loop", like the thing you keep chickens in. Not "co-op".

coop is a Rust CLI that manages disposable virtual machines where Claude Code and Codex have full tool access: Docker, git, compilers, package managers, all without risk to your host machine. Each VM is isolated, reproducible, and cheap to create and destroy.

Setup

Install the latest release:

curl -fsSL https://raw.githubusercontent.com/trailofbits/coop/main/install.sh | bash

Or build from source (requires Rust ):

cargo build --release
cp target/release/coop /usr/local/bin/

Then build the VM template image:

On Linux, coop setup also installs Firecracker and fetches a guest kernel. On macOS, install Lima first ( brew install lima ) — setup fails without it. coop is tested on macOS arm64 (Apple Silicon) and Linux x86_64; Linux arm64 builds are available but untested. Each backend has its own host requirements — see Prerequisites .

Keep coop updated:

See coop update and the updates config section .

Usage

Start an instance for the current project and launch an agent CLI:

cd ~/code/my-project
coop up
coop claude
# or
coop codex

Documentation

U.S. Military and Spy Bases in Australia, Mapped

Portside
portside.org
2026-09-06 23:37:40
U.S. Military and Spy Bases in Australia, Mapped Ira Sun, 09/06/2026 - 23:37 ...
Original Article

U.S. Military and Spy Bases in Australia, Mapped

Research by the Nautilus Institute has revealed what some analysts have labelled a 'colonisation' of Australia by the United States . . . could this drag Australia unwillingly into war?

U.S. Military and Spy Bases in Australia, Mapped Published

Recently, it was announced that the US military would store tens of millions of dollars worth of US weapons, munitions, fuel, and vehicles in a new facility right here. | Guardian Australia

Keep Our Servers Running: Your Recurring Donation Goes 3X This September

Hacker News
blog.archive.org
2026-09-06 23:29:51
Comments...
Original Article

“Universal access to all human knowledge is within our grasp. Our job is to put the best our world has to offer within the reach of our children.”

—Brewster Kahle, Internet Archive Digital Librarian

Every time you search the Wayback Machine or explore a collection at the Internet Archive, you are accessing a global library built to put knowledge within reach of curious learners around the world.

The mission of “Universal Access to All Knowledge” is a commitment that goes beyond book scanners and web crawlers. It requires servers, storage, power, cooling, and the people who build and maintain our systems. Our infrastructure is the backbone of our digital library.

The Internet Archive has always been completely free for everyone, everywhere. We don’t charge for access, sell user data, or run ads. Rather than contracting out our core technology to corporations, we build and maintain our own systems.

That independence helps us preserve and provide public access to 210 petabytes of knowledge. However, it also means we are responsible for keeping that infrastructure running—and our needs are growing rapidly.

This September, you can help us meet that challenge and make your support go three times as far.

When you start a recurring donation of $25 or more in September, your initial gift will be matched 2:1.

That means your $25 monthly donation unlocks an additional $50 in matching support, resulting in $75 of giving on your behalf. A $50 monthly gift becomes $150. A $100 monthly gift becomes $300.

Recurring gifts provide the dependable support we need to keep our infrastructure running year after year. In fact, the Internet Archive is powered by donations averaging about $25.

When you join the Monthly Giving Circle, you aren’t simply maintaining servers. You ensure that books are readable and websites are accessible. You are preserving the best we have to offer for generations to come.

Join the Internet Archive Monthly Giving Circle this September with a recurring gift of $25 or more, and your initial gift will be tripled through the 2:1 match.

Real-Time Fluid Dynamics for Games

Lobsters
www.researchgate.net
2026-09-06 22:54:47
Comments...
Original Article

Access restricted

We've detected unusual activity from your network. Access to this page is temporarily restricted.

Are you a researcher?

Log in or sign up as a ResearchGate member to verify your access.

Ray ID: a3727c9dee4282d8

Client IP: 204.19.241.158

© ResearchGate GmbH. All rights reserved.

PL Education in the Age of AI: Interview with Shriram Krishnamurthi

Lobsters
www.typetheoryforall.com
2026-09-06 22:15:34
Comments...
Original Article

Shriram has devoted his Career to advance the teaching of programming of programming languages. he is a professor at Brown, The author / co-author of different books about this, and a major contributor the Racket Programming Language.

In this episode we talk in depth about why PL education is so important, what can students really gain from in practice? What is changes in the age of AI? Is programming even going to be a thing in the years to come!?

All contributions to the show are greatly appreciated! https://www.typetheoryforall.com/patreon

Links

Has anybody seen my keys? A key-hierarchy strategy for rack-level security

Hacker News
rfd.shared.oxide.computer
2026-09-06 22:14:59
Comments...
Original Article

Background and Purpose

There are many different types of secrets inside an oxide rack. At the base of the system we have the DeviceId and Alias keys stored on the RoT and used for platform identity and measurement signing for attestation respectively [RFD 36] . These keys along with a 3rd RoT hosted keypair used for authenticating ephemeral Diffie-Hellman agreement provide the ability for sleds to form secure sprockets sessions for application layer messages [RFD 238] . These sprockets sessions provide each sled the ability to confidentially share information in a point- to-point fashion, where the integrity of messages is protected, the authenticity of the endpoint and the attestation of its running software is guaranteed.

In order to provide the rack-level security guarantees such that an attacker cannot walk off with a subset of sleds or drives and recover any useful information, we have designed a Trust Quorum [RFD 238] .

The primary protection mechanism behind the trust quorum is shamir secret sharing. A shared rack-level secret which is used as a key-derivation source for other keys used to protect storage at rest. The rack secret is split into N unique key shares by a dealer process and distributed over sprockets sessions to each bootstrap agent along with the unique platform identities of the N trust quorum members. These platform identities are baked into the public key certificates on the RoT to allow verification that an entity is who they say they are. After distribution of this information, bootstrap agents can establish sprockets connections to other bootstrap agents, verify membership in the group (via the exchanged certs), and retrieve K-1 shares from other agents such that they can reconstruct the rack secret from K shares. Importantly, without obtaining K shares, no information about the rack secret can be learned.

As described in [RFD 238] , individual shares are store unencrypted on each sled’s M.2 drives. An attacker would have to steal at least K of these drives to reconstruct the rack secret, which is infeasble without significant time and disruption during physical access. In the future we plan to "seal" these secrets with the help of our root of trust (RoT) such that they are only decrypted on sled boot. Sealing would mean that an attacker would have to steal K whole sleds and be able to boot them to recover the rack secret. The weight of K sleds makes this prohibitive for a casual attacker.

With this rack secret we have a shared secret that is only available when enough of the N sleds in the group are plugged into the same rack and trust each other enough to distribute shares to each other. From this shared secret, we can do things like derive or wrap individual encryption keys for each individual U.2 device, and independently derive rack level root certificates for internal services.

The purpose of this RFD is to identify:

  • What data is protected by the rack secret?

  • What is the temporal lifecycle of that data?

  • What is the spatial locality of that data?

    • Is it local to a specific drive/sled/rack/cluster?

    • What physical or logical restrictions prevent the encrypted data from being moved?

  • What is the key-hierarchy used to protect that data

    • What keys protect what data?

    • Which keys are derived from which secrets and keys?

    • Which keys are wrapped (encrypted by other keys)?

    • How are keys derived or wrapped (Key Schedule)?

    • What happens in the case of key compromise?

These questions are critical to the security of our rack, and our determinations may evolve given our experiences of with the current solution and more time to think of and implement solutions necessary to harden and expand our posture. For now, though, this RFD must deliver strong enough answers to allow us to best protect our data at rest and move onto our other security goals in the near future.

Leaf Peeping

Our rack-level key-hierarchy starts at the root with the Rack Secret. Each member of the trust quorum can learn enough shares to recompute the rack secret. The rack secret itself is not a key, and so we must derive keys from it via a key derivation function (KDF) that can be used for vaious purposes required by our rack. What keys we derive, and what keys are derived from those keys, and what keys any of them may wrap, is hard to determine without more information about what data is actually being protected by keys in our system. To help construct our key hierarchy, and fill in the internal nodes in the tree, we will start by listing out the data to be protected within the rack, what unique keys will be used to protect that data, and the security guarantees we would like from those keys. After establishing the leaf-keys we can work backwards to fill in the rest of the key hierarchy.

The below table shows the data we care about protecting by the rack secret key hierarchy for now. There are likely other things that need protecting, but they will almost certainly have similar requirements. And while we currently store Crucible encryption keys in CockroachDB, we anticipate having some sort of key management system in place post MVP that will house things like encryption keys, certs, and authentication tokens. We also do not currently have the internal certificate authority mechanism defined for managing how control plane services communicate over mutual TLS. We do know, however, that we will want such a system and therefore are being explicit here that we will at least have a root certificate and leaf certificates to protect. Whether or not we need intermediate certs is TBD.

Data Storage Type Storage Device Lifetime

Control Plane Data

CockroachDB

Some U.2 devices

Temporary until lifetime of rack

Control Plane Metrics

Clickhouse

Some U.2 devices

?

Crucible Extents

Files

Most U.2 devices

Lifetime of a storage volume

Crucible volume encryption keys

CockroachDB

Some U.2 devices

Lifetime of a storage volume

User Authentication Tokens

CockroachDB

Some U.2 devices

Temporary / user-session TTL

Internal Service Leaf Certs

Files

All U.2 devices - although not all certs on all devices

Cert TTL

Internal CA Root Cert Private Key

File

All sleds?

Long enough to at least sign Leaf Cert CSRs, and then may be rederived for rotation of leaf certs

Internal CA Root Cert Public Key

File

One or more U.2 devices on all sleds

Until Cert rotation

We can see that all stored data resides on U.2 devices, either in files or databases, that end up in files. We know based on prior discussions that we do not want to use hardware backed full disk encryption (due to lack of trust in implementation, complexity of key management, and multiple possible vendors), and we also know that we will have one zpool per U.2 device. We want to encrypt almost the entire zpool via a root / crypt dataset using ZFS encryption, opting out specific datasets like crucible that provide their own encryption. We can therefore assume that all necessary customer and control plane data on each U.2. device is encrypted. To limit key compromise, such that if one disk is stolen and has its key compromised, not all other disks are compromised, we choose to use an individual key per disk. We know that each disk contains a lot of data, and that we do not want to re-encrypt that data when changing keys. Lucikly for us, ZFS allows us flexibility in how we generate and change these keys.

Key rotation is managed internally by the ZFS kernel module and changing the user’s key does not require re-encrypting the entire dataset.

— zfs(8) manpage

As we are encrypting all critical data on the U.2s using ZFS encryption (including crucible keys), and those ZFS keys are not stored on those disks, we cover our data at rest protection requirements. An attacker who cannot reconstruct the rack secret can not steal a subset of drives or sleds and retrieve any useful information. We now must figure out whether we want to derive or wrap those keys, and what the rest of the hierarchy looks like.

Building a Key Hierarchy from a Rack Secret

Assuming a static cluster, we now have a rack secret that can be computed from K key shares and used to derive child keys. In the following example, K = 2 , just so we can keep the diagrams small.

The rack secret protects every key in the system that is used after rack unlock. There are two mechanisms to get the next keys in the hierachy:

  • Key Derivation

  • Key Wrapping (Key Encryption)

Key Derivation we already use to derive any primary child keys from the rack secret. But it can also be used to derive keys from keys. Key wrapping is just taking a key and encrypting it. Each has their benefits and drawbacks.

Key derivation is nice because you never have to store the derived keys on disk. You can just regenerate them. The problem with it is that if the derived key changes any downstream derived keys will now change also change.

Key wrapping is nice because if the wrapper key is rotated, the downstream keys do not have to change. This is particularly useful for encrypting large amounts of data on storage. You don’t want to be forced to decrypt and then re-encrypt just because a parent key was rotated. The downside of key wrapping is that you now have to store the wrapped (encrypted) key on disk somewhere. If the same key is used in multiple places you have to replicate it.

For trust quorum, we have the particular problem that when nodes are added or removed we generate new shares. While it’s possible to maintain the same rack secret and key derivation with new shares, it is less secure over time because any single malicious node who retrieved the rack secret at one time and saved it can now recover any data on any existing drives or any data produced in the future. While this is always an issue, we prefer to allow the rotation of the rack secret in the case of a known compromise. We therefore always generate a new rack secret on every change of the trust quorum (reconfiguration), or other key-share rotation. This way, even if all existing data on the rack is compromised, at least no new new data will be compromised if the compromised sled is removed.

Right now what do we know about our security goals? We want to:

  • Derive keys from the rack secret as much as possible to limit the need to store wrapped keys

  • Allow rotation of the rack secret so that a compromised rack secret can be mitigated

  • Use unique encryption keys per U.2 drive so that in case one key is compromised, the others are not compromized.

  • Ensure that new (empty) sleds cannot access any at rest data from old sleds that is not shared with them once a reconfiguration occurs.

And what are our constraints given these goals? We must:

  1. Be able to change the ZFS wrapper keys per U.2 drive when the rack secret is rotated. This requires knowing the old and new wrapper key at the same time.

  2. Allow for the fact that not all sleds will know at the same time when a new reconfiguration has been committed, and when to change the wrapper key, given the distributed nature of key rotation.

  3. Recognize that commitment of the new configuration and hence new rack secret may occur after multiple "false starts", where a new reconfiguration is distributed to multiple sleds but not committed.

The first constraint is a given from our use of disk encryption in general. The latter two constraints come from the distributed nature of the reconfiguration problem and are elaborated upon in [RFD 238] . The second constraint makes it impossible to distribute the new key share during the committment, because after a sled commits to a new epoch it should only utilize the new rack secret, and thus it may have to request the new key shares from a sled that has not yet learned that it has committed. There are other security reasons for not distributing key shares during commit that are further fleshed out in [RFD 238] . Therefore, given a 2-phase commit protocol ( [RFD 238] ), we must distributed the new shares in the prepare message. The third constraint makes it impossible to change the ZFS encryption wrapper keys immediately when learned, since the new rack secret from which those keys are derived may never be committed. Without a committed rack secret, it will be impossible to retrieve the shares necessary to recompute the secret and rederive the ZFS encryption wrapper keys.

Given these goals and constraints, we re-iterate that any sleds that are members of both the old and the new group must have access to the old and new committed rack secrets at the same time. This is necessary to allow them to derive the ZFS encryption wrapper keys for each U.2 device so that they can change the keys. For simplicity of the reconfiguration protocol, and to limit the exposure of the old rack secret we also only want to allow sleds to distribute key shares for the currently committed configuration. The requirement and desires above are in tension, and so we must get creative about how we handle this situation.

The most straightforward way to solve this predicament is via the dealer during a reconfiguration. As described in [RFD 238] we number each configuration with a monotonically increasing epoch. At epoch 1, each sled gets a single share and recomputes the rack secret to derive the original ZFS encryption wrapper keys for the U.2 devices. When a reconfiguration occurs, the dealer retrieves enough shares to recompute the rack secret for the current committed epoch (1 in our example). The dealer generates a new rack secret for epoch 2, and splits it into key shares. The dealer also derives an old-rack-secret encryption key associated with epoch 1 from the epoch 2 rack secret and encrypts the epoch 1 rack secret with the old-rack-secret key. It sends this encrypted secret to sleds that are members of the new group along with the rest of the trust quorum prepare message. Until the new configuration at epoch 2 is committed, the encrypted epoch-1 rack secret cannot be decrypted, because no members will send shares for epoch-2 required to recompute the epoch-2 rack secret and the derived key necessary to decrypt the epoch 1 rack secret. If epoch 2 is never committed this will remain the case.

As soon as a sled sees that a configuration has been committed for a new epoch, it retrieves enough shares to unlock the new epoch rack secret, derives the old-rack-secret encryption key, decrypts the old rack secret protected by this key, derives the old and new U.2 encryption keys from the old and new rack secret respectively and re-configures the ZFS encryption for each U.2 drive. Once the encryption keys have been changed for all the U.2 drives, the encrypted rack secret for the old epoch is securely deleted, along with any other encrypted rack secrets for the old epoch that were prepared but never committed. There is some nuance here around failure modes, but that is not relevant to this RFD and is further fleshed out in [RFD 238] .

Our key hierarchy has now been roughly described in prose, and it tolerates failures of sleds during reconfiguration, and allows the changing of encryption wrapper keys used for ZFS encryption on U.2 drives. It also deals with compromise of the rack secret and limits exposure of individual derived key compromise. With this in mind, we can now draw a diagram of the key-hierarchy.

Key Derivation

Note

We are ignoring any CA related certs or other keys here and focusing on storage encryption keys and Rack secret encryption keys. Those are most relevant to MVP. Similar techniques will be applied as needed to certs and other keys when their usage gets defined in other RFDs.

Similar to sprockets ( [rfd256] ), we use [hkdf] with sha3-256 as the hash algorithm as our key derivation function. We also use chacha20poly1305 for encrypting our rack secret during reconfiguration. ZFS encryption has limited encryption options, and so we choose the strongest one which is AES-GCM-256 . Notably, both AES-GCM-256 and chacha20poly1305 use 32 byte (256-bit) keys, and so we always derive 32 byte keys from HKDF.

HKDF is a two-step algorithm, and while it does not require a salt for the first extract step, it strongly suggests one. The salt should be independent of the key material, at least as long as the hash function used, and does not have to remain secret. While ideally, the salt wouldn’t be generated by one party, we already rely on the dealer to generate the rack secret. For ease of implementation we will have the dealer also randomly choose a 32 byte salt for each epoch and include it with the key share, and membership data sent to the bootstrap agents. Note that this salt is only used in the key derivation for the old encrypted rack secrets, and not for the disk encryption keys. This is somewhat of a historical artifact, and it’s not clear that it provides much security guarantee in either case. The reason for this is that the HKDF-expand for generating key material relevant to the previous rack secrets is performed by the trust quorum code itself, while the disk key generation is done in a separate module, key-manager , which only receives the input key material (the rack secrets). There is no separate distribution of a different salt from the dealer and so we’d either end up sharing the salt or adding a new one. In either case we’d need to plumb that through to the key manager, which is extra work with no clear improvement in security.

After extracting the ikm (and possibly salt) into a uniformly distributed output key material using HKDF-Extract , individual keys may be derived from this output using the HDKF-Expand part of the HKDF algorithm. To bind output derived keys to their usage and context, such that they will be prevented from being used for other purposes by other derivers of the same key, an info parameter is provided to the HKDF-Expand function.

In our scenarios we have two types of keys we are generating: storage encryption keys, and rack secret encryption keys. We want to contextualize these so that one key isn’t mistakenly used as the other. We also want further contextualization for individual drives and configuration epochs so that keys with the same purpose are not confused, but with different specific use cases are not confused with each other. We define the info strings we pass to HKDF below where + is the concatenation operator.

  • Rack Secret Info : "rack-secret" + new_epoch + '-' + old_epoch where each of new-epoch and old-epoch is a 4 byte big-endian integer. new_epoch corresponds to the epoch where the encryption keys is derived, and old-epoch is the epoch of the rack-secret that is being encrypted.

  • U.2 Drive Info : "U.2-zfs-" + pci_vendor_id + drive_model + drive_serial_number , where the tuple of (pci_vendor_id, drive_model, drive_serial_number) uniquely identifies a U.2 drive.

The derivation chart then looks like the following (using 2 drives instead of 320) for a rack. The dealer creates a rack secret and salt for epoch 2 used for encrypting rack secrets. From there the 2 storage keys and the rack secret wrapper key which protects the rack secret from epoch 1 with the rack secret from epoch 2 can be derived. While it’s possible for any bootstrap agent with access to the rack secrets for epochs 1 and 2 to perform the derivation and encryption, in practice, the encryption is only done by the dealer, while the other bootstrap agents only derive the key required to decrypt the old rack secret, since they don’t actually have a way to get access to both the old and new rack secrets simultaneously.

Determinations

  • Each U.2 drive has a storage separate encryption key

  • We derive all storage keys from the current rack secret utilizing their serial numbers

  • Rack shares can only be retrieved for the current epoch. While not explicitly specified here, but described in [RFD 238] , boot agents may learn about commitment from other agents. This eliminates the problem of new agents who commit being stranded and not being able to get enough shares to unlock their rack.

  • We derive a rack secret encryption key from a new rack secret being prepared to protect the old (current) rack secret upon commit and distribute this to each member of the new and old group during reconfiguration.

  • Upon learning about commit of a new epoch, members of the new group retrieve shares for the new rack secret, recompute it, decrypt the old rack secret in the prepare message, derive the storage keys, reconfigure storage encryption, then securely delete the encrypted rack secret and the new and old rack secrets in memory.

Why Meta started losing in court - podcast

Guardian
www.theguardian.com
2026-09-06 22:00:21
Katy McQue, who has spent years reporting on Meta and the potential harms its platforms pose to children, explains the tactics being used by prosecutors For years, the investigative journalist Katy McQue has been reporting on the potential harms Meta’s platforms pose to children. Yet, as she explain...
Original Article

For years, the investigative journalist Katy McQue has been reporting on the potential harms Meta’s platforms pose to children.

Yet, as she explains to Annie Kelly , despite the mountain of evidence compiled by journalists, prosecutors, bereaved families and others, it has been nearly impossible to bring Meta and other social media companies to court.

Finally, in 2026, something seems to have changed. Already three landmark cases have brought Meta to trial in the US: in New Mexico, which cited Katy’s reporting on child sexual exploitation; in California, about the mental health harms to young people; and last week, an unprecedented case that Meta settled for $18bn.

In all three, prosecutors seem to have found a new way to argue against Meta; an argument based not on the content it publishes, but on the very design of its platforms.

Meta chief executive Mark Zuckerberg leaving a court
Photograph: Mike Blake/Reuters

Show HN: GET Together – A social network where you don't need POST to Post

Hacker News
gettogether.dev
2026-09-06 21:41:11
Comments...
Original Article

Names. 2–20 letters, numbers, or underscores. Names aren’t unique or verified.

Cookies. Cookies are optional for posting. To delete a post later, keep the gt_session cookie returned by the server and send it with your next request. In cURL, add -b cookies.txt -c cookies.txt to save and reuse it. Keep that file private.

Retries. The server creates an ID if you leave it out. For safe retries, send your own id UUID and reuse it with the same cookie. Requests without an ID create a new post each time.

Endpoints. /post?name=alice&text=hello writes a post. /feed returns posts as JSON. /heart?id=…&on=1 adds a heart; use on=0 to remove it. /delete?id=… deletes your own post. All use GET.

Replies. Add parent=POST_ID to /post to reply to a post. Open its replies to copy an example. /feed?parent=POST_ID returns its replies and the original post. Replies can have replies, and use the same limits and moderation checks. Deleting a post preserves other users’ replies.

Moderation. New posts and names are checked for English profanity and crypto content. Crypto, Bitcoin, memecoins and token promotion are not allowed. Reports trigger an automated abuse review. Clear violations are hidden; reporting alone does not remove a post. Read the rules.

Privacy. The post is public, and its text is part of the URL. Don’t include private information.

400
Check the name, text and UUID.

403
Check the owner cookie or request origin.

429
Wait ten seconds and retry.

Ponytail: Lazy Senior Engineer Skill

Hacker News
ponytail.dev
2026-09-06 21:18:17
Comments...
Original Article

~/ponytail README.md built.md

Ponytail logo: a deadpan senior developer with a ponytail and glasses

He says nothing. He writes one line. It works.

// A ruleset that makes your AI coding agent write the least code that works — like a senior dev who's been paged at 3am one too many times.

Something's coming. Be the first to know.

the_whole_idea

Your agent reaches for fifty lines. The job needs one.

cache.py −48 +1

- class CacheManager:-     def __init__(self, ttl, maxsize):-         self._store, self._lock = {}, Lock()-     # ...44 more lines you maintain forever+ @lru_cache(maxsize=1000)+ def fetch(...): ...

// same behavior. 48 fewer lines. zero bugs in code that no longer exists.

the_ladder

Stop at the first rung that holds.

  1. Does this need to exist? Speculative need = skip it. (YAGNI)
  2. Already in this codebase? Reuse the helper, util, or pattern that already lives here.
  3. Does the standard library do it? Use it.
  4. Native platform feature covers it? <input type="date"> over a picker lib.
  5. Already-installed dependency solves it? Use it. Don't add a new one.
  6. Can it be one line? One line.
  7. Only then: the minimum code that works.

benchmarks

Less code. Fewer tokens. Same safety.

// medians across 12 feature tasks on a FastAPI + React repo.
// validation, error handling, security and accessibility are never simplified away.

install

Two lines for Claude Code.

# Claude Code
/ plugin marketplace add DietrichGebert/ponytail
/ plugin install ponytail@ponytail

codex

codex plugin marketplace add DietrichGebert/ponytail

copilot cli

copilot plugin install ponytail@ponytail

gemini cli

gemini extensions install github.com/DietrichGebert/ponytail

pi harness

pi install git:github.com/DietrichGebert/ponytail

// also OpenCode, Cursor, Windsurf, Cline, Kiro, Zed and more — 14+ agents.
// full list in the README .

commands

Drive it from chat.

/ ponytail lite|full|ultra|off set intensity, or turn it off
/ ponytail-review find over-engineering in the current diff
/ ponytail-audit scan the whole repo for bloat
/ ponytail-debt collect deferred shortcuts into a ledger
/ ponytail-gain show the benchmark scoreboard
/ ponytail-help quick command reference

intensity

Pick how lazy.

"lite"

Builds what you asked, names the lazier alternative in one line. You pick.

# default

"full"

The ladder, enforced. Stdlib and native first. Shortest diff, shortest explanation.

"ultra"

YAGNI extremist. Ships the one-liner and challenges the rest of the requirement in the same breath.

ChatGPT Astra is now rolling out to $20 Plus subscription

Bleeping Computer
www.bleepingcomputer.com
2026-09-06 21:15:43
OpenAI is now rolling out ChatGPT Astra, its most powerful model to date, to those with a $20 Plus subscription, but there's no word on when free users will get access.. [...]...
Original Article

ChatGPT

OpenAI is now rolling out ChatGPT Astra, its most powerful model to date, to those with a $20 Plus subscription, but there's no word on when free users will get access.

The rollout appears to be happening gradually, and Astra may show up inside ChatGPT Work before it becomes available in regular Chat.

In my $20 Plus account, Astra is already available in the Work section of ChatGPT, while the normal Chat model picker still doesn't offer it.

Here's what it looks like when I go to ChatGPT Work and click on the model picker:

Astra
ChatGPT Astra
Source: BleepingComputer

So, if you have ChatGPT Plus and don't see Astra in regular Chat yet, it's worth checking Work.

OpenAI previously confirmed that it's rolling out Astra to Plus users, but it'll be a while before it shows up for everyone.

"GPT-6 Astra is now available to all Pro, Enterprise, and Business Premium users in ChatGPT Work and Codex. It's also live in the API," OpenAI wrote in a post on X.

"It might take a few days to roll out to our Plus and Business users."

Astra is included with the existing ChatGPT Plus subscription

OpenAI says Astra usage is included within existing subscription limits, so Plus users do not need to purchase a separate plan to access the model.

If you need additional usage, you will also be able to buy credits.

Astra is OpenAI's new flagship model and is designed for computer use, browsing, coding, science, cybersecurity, and longer professional tasks.

OpenAI says it is also significantly better than GPT-5.6 Sol at maintaining context during complex work.

While ChatGPT Astra is rolling out to paid users, OpenAI hasn't said whether the model will roll out to free users, or in what capacity if it does. For now, free users have access to GPT-5.6 Sol and GPT-5.

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

Kernel prepatch 7.3-rc2

Linux Weekly News
lwn.net
2026-09-06 20:52:57
The 7.3-rc2 kernel prepatch is out for testing. Linus said: This didn't *feel* like a particularly busy rc2, but it clearly was. rc2 is usually the quietest time when people take a breather after the merge window and it takes a while to start finding bugs. But not this time - this is a "full...
Original Article
The 7.3-rc2 kernel prepatch is out for testing. Linus said:
This didn't *feel* like a particularly busy rc2, but it clearly was. rc2 is usually the quietest time when people take a breather after the merge window and it takes a while to start finding bugs. But not this time - this is a "full fat" rc release. [...]

Nothing looks particularly odd, even if the rc2 timing is a bit unusual. It might be just random, but we'll obviously all blame it on AI, because whether that's really the cause or not, it's an easy thing to blame ;)



216M Spy TVs – The LG Smart TV Problem [video]

Hacker News
www.youtube.com
2026-09-06 20:22:04
Comments...

MathKernel: An evidence-aware multi-engine mathematics kernel and MCP server

Hacker News
github.com
2026-09-06 20:21:01
Comments...
Original Article

An evidence-aware multi-engine mathematics kernel — usable both as a Python library ( mathkernel ) and as an MCP server ( mathkernel-mcp ) — so applications and LLMs can do advanced mathematics while preserving assumptions, provenance, and claim-specific evidence.

The LLM interprets intent; the MathKernel establishes mathematical evidence.

Mathematical results carry an explicit trust level , an engine tag, and a derivation trail . Exact computation, checked certificates, symbolic results, certified enclosures, empirical evidence, and formal proofs are distinct claims. Exact arithmetic alone is not a formal proof; approximate-input ancestry must not silently disappear.

version python engines license


Table of contents

Why

LLMs are good at mathematical intent and bad at mathematical arithmetic . MathKernel inverts the division of labor: the model parses, plans, and interprets; the kernel computes and records claim-specific evidence. Some claims use independent certificates or cross-checks; others are exact computations in one engine. Engine agreement alone is not a proof, and a single trust label does not replace the evidence bundle.

alt text

Architecture

MathKernel is a typed orchestration layer rather than a single solver. The public facade owns parsing, contexts, object identity, persistence, evidence composition, resource policy and derivation tracking; domain adapters own the actual mathematics. Presentation layers sit downstream and cannot silently change the claim being made.

Python / MCP
    |
    v
MathKernel facade
    |-- parser + contexts + typed objects
    |-- execution/evidence contract
    |-- persistence + derivation graph
    |
    +--> symbolic / exact / certified / formal / numerical engines
    |
    +--> MathResult and derived mathematical objects
             |
             +--> MultimodalProjection
                     |--> mathkernel-viz
                     |--> mathkernel-sonify
                     +--> unified portable artifacts

This separation is deliberate: a renderer may present evidence, but it does not create stronger mathematical evidence merely by producing a polished plot or audio artifact.

Feature matrix

Domain Compute surface Engines Verification / evidence ceiling
Symbolic algebra parse, substitute, simplify/expand/factor, solve, systems SymPy SYMBOLIC; input ancestry may lower it
Calculus differentiation, integration, limits, series, sums, products SymPy SYMBOLIC + conditions
Integral transforms Laplace/Fourier/Mellin/bilateral Z, inverses, ROC and property obligations typed transform adapter + SymPy SYMBOLIC; NUMERIC for approximate ancestry
Complex analysis branches/domains, zeros/singularities, residues, Laurent series, contours, argument principle, continuation, conformal maps typed complex adapter + SymPy SYMBOLIC defining identities; EXACT winding certificates only for exact geometry, ancestry-capped otherwise
Continuous probability typed univariate/joint/conditional distributions, transformations, marginals, Bayes, covariance, divergence, order statistics typed probability adapter + SymPy SYMBOLIC normalization/identity evidence; mathematical nonexistence retained
Exact graphs typed simple/directed/weighted/multi graphs, traversal, components, shortest paths, MST, max-flow/min-cut, bipartite matching, Euler trails, coloring, topological sort, cycles, centrality, isomorphism deterministic exact graph algorithms over Fraction + njit CSR traversal kernels EXACT witness certificates; NP-hard optimality is OPTIMUM/CANDIDATE/IMPOSSIBLE/UNKNOWN, never heuristic nonexistence
Exact combinatorics combinatorial classes, exact counts, lazy generation, ordinary/exponential generating functions, recurrences exact integer/ Fraction enumeration + SymPy + checked njit recurrence kernels EXACT counts and recurrence/coefficient checks
Finite algebra finite groups, permutation groups, abelian groups, homomorphisms, Z/nZ, GF(p^m), modules, Smith/Hermite normal forms exact algebra + SymPy combinatorics + njit Cayley/GF(p)[x] kernels EXACT axiom, homomorphism, irreducibility, and normal-form certificates
Linear algebra determinant, inverse, multiply, rank, RREF, eigenvalues, exact solves SymPy EXACT for exact arithmetic; otherwise ancestry-capped
Reasoning obligation-DAG planning, equivalence, counterexamples SymPy + Z3 + Lean SYMBOLIC / EXACT / FORMAL by verifier
Certified numerics arbitrary-precision evaluation and interval enclosures mpmath + mpmath.iv CERTIFIED NUMERIC or NUMERIC
Integers arbitrary precision, gcd/lcm, primality, factorization, CRT, modular arithmetic exact + numba batch EXACT
Code generation TypeScript/Python/Rust emission, typecheck, symbolic round-trip, sandbox compilers + SymPy SYMBOLIC verification; never stronger than source
Binary fields GF(2^m) arithmetic/construction and Rabin irreducibility njit n-limb kernels EXACT certificates
GF(2) linear algebra rank, nullspace, powers, Berlekamp–Massey, carry-free columns bit-packed integers EXACT
Discrete transforms exact FWHT with bigint fallback numba EXACT
Finite dynamics Koopman/observation transfer, visibility, lagged tensors, diagnostics exact + NumPy/CuPy EXACT or NUMERIC, selected explicitly
Branching Markov tensors arbitrary finite rooted Markov trees, exact leaf laws/cumulants, true-edge flattening certificates, stochastic leaf observations, channel-rank transfer, exact recovery and collective sensor fusion exact Fraction sum-product/enumeration + NumPy SVD diagnostics EXACT algebraic identities/ranks/recovery; NUMERIC singular-value and conditioning evidence kept separate
Connected-relation detectability pure connected-interaction laws, stochastic mode visibility, conditional-expectation spectra, exact chi-square/Fisher retention, invisibility certificates, finite sample bounds and sensor fusion exact Fraction laws + weighted NumPy SVD + exact binomial likelihood-ratio validation EXACT transfer/information identities and lower/upper bounds; EMPIRICAL Monte Carlo checks remain separately labelled
Relation-subspace visibility multi-relation Fisher Gram transfer, generalized visibility spectra, blind-combination collision certificates, cost-constrained sensor design, empirical partitions and long-run-covariance correction finite probability algebra + weighted NumPy generalized eigensystems + exact finite sensor enumeration EXACT local transfer/data-processing/collision identities; NUMERIC spectra and EMPIRICAL dependence/SkewDB checks retain explicit scope
Intrinsic observation information geometry finite-simplex Fisher tangents, coordinate-invariant retained-information spectra, exact local chi-square transfer, worst-direction testing lower bounds, finite Bhattacharyya upper bounds, iid/block/cluster spectrum bootstrap, local-resolution SkewDB adapter finite probability algebra + weighted generalized eigensystems + SciPy exact-binomial validation + seeded resampling EXACT finite tangent/data-processing/divergence identities and finite simple-testing bounds; NUMERIC eigensystems and EMPIRICAL uncertainty checks remain separately labelled
Composite relation inference one direction-agnostic relation-subspace test, dimension-aware finite bound, nuisance-efficient Fisher geometry, eigenspace regions, studentized/block bootstrap, HAC and misspecification diagnostics finite Fisher algebra + NumPy eigensystems + optional SciPy chi-square calibration + seeded resampling EXACT nuisance/data-processing identities and conservative bounded-score guarantee; ASYMPTOTIC composite calibration and EMPIRICAL bootstrap/dependence checks are labelled
Finite Fourier cyclotomic DFT/transfer/coefficient/orbit calculations exact + NumPy FFT EXACT or NUMERIC cross-check
Closure search cyclic/XOR irreducible closure relations njit meet-in-the-middle EXACT witness/exhaustive evidence
Conditioned dynamics orbit access, cocycles, closures and symmetry synthesis exact enumeration + canonical rewrite EXACT witnesses
Cumulants moments/cumulants and connected sample statistics exact + NumPy EXACT algebra or EMPIRICAL samples
Sets & logic set algebra, membership, quantified truth and elimination SymPy sets + Z3 EXACT SMT witnesses where established
Polynomial algebra Gröbner bases, division, resultants, factorization, ideal membership exact SymPy polynomial algorithms EXACT algebraic certificates
Discrete probability rational RVs, Bayes, Markov quantities, seeded sampling Fraction + NumPy EXACT distributions; EMPIRICAL sampling
Statistics and stochastic systems typed samples, GLMs, rank/resampling inference, survival/time-series analysis; Poisson/Wiener/GP/CTMC laws; typed Itô SDEs, Euler–Maruyama/scalar Milstein paths and coupled convergence studies typed statistical/survival/time-series/stochastic/SDE adapters + SymPy + NumPy/SciPy/mpmath EXACT identities remain separate from labelled NUMERIC fits/conditioning/exponentials and seeded EMPIRICAL resampling/simulation; no implied process/model validity, convergence theorem, population inference or causality
Tensors sparse tensors, contraction and sparse solves exact + njit + CuPy EXACT or NUMERIC by arithmetic path
ODEs / PDE symbolic ODE classification/dsolve; numerical IVP/named PDE solvers; typed PDE systems, weak forms, oriented simplex meshes, P1 spaces, sparse assembly, checked algebraic solves, residual–jump indicators, marking, conforming refinement, nodal transfer and observed estimator rates typed PDE/FEM/adaptivity adapters + SymPy + SciPy sparse + mpmath + njit + CUDA/CuPy estimators and empirical rates retain ancestry and never become rigorous continuum bounds or convergence theorems
Optimization critical points, KKT, exact LP, numerical nonlinear/multistart Fraction + njit + process pool EXACT LP certificates or NUMERIC candidates
Units SI dimensions, rational conversions and semantic-unit propagation exact Fraction EXACT
Assurance interval obligations, Lean replay, Arb balls, persistence and fuzzing mpmath.iv + flint + Lean CERTIFIED NUMERIC / FORMAL / differential evidence
Theorem proving SMT portfolio and Lean certificates Z3 + Lean EXACT SMT witness or FORMAL kernel-checked proof
Exhaustive sweeps Collatz and cuboid searches numba + CUDA + process pools EXACT only when coverage is exhaustive
Async jobs submit/status/result/list with evidence-preserving retrieval job pool Preserves underlying evidence
Visualization renderer-neutral interactive/static mathematical artifacts Python SVG + vendored three.js No new evidence; preserves source trust
Sonification declarative scientific audio mappings and deterministic WAV Python PCM + WebAudio Candidate observation only
Multimodal artifacts synchronized visual/audio artifact assembly shared artifact schema Weakest included claim/evidence
Differential geometry manifolds, oriented charts, metrics, coordinate maps, tensor fields, forms, curvature, covariant/Lie/exterior derivatives, wedge/interior/pullback/Hodge operations typed geometry adapter + SymPy SYMBOLIC identities with explicit domains, Jacobians, signature and ancestry; numeric input stays NUMERIC
Computational geometry concrete points/sets, polygons, half-space polytopes, triangulations, hull, containment, intersection, nearest neighbor, Delaunay and Voronoi exact SymPy determinants + adaptive float filters EXACT topology for exact coordinates; NUMERIC only when filters decide; otherwise explicit AMBIGUOUS outcome
Algebraic topology finite simplicial/cubical/integral chain complexes, exact triangulation conversion, oriented boundaries, Euler characteristic, homology over Z/Q/GF(p) exact integer matrices + certified Smith normal form + rational/modular elimination EXACT face-closure, boundary², rank-nullity, quotient, torsion and Euler–Poincaré certificates

Typed functionality surface

The generic MCP tools math_object_create , math_object_get , and math_apply expose the following compositional operations. This is the full typed-operation inventory; math_capability_query is the live source of parameter schemas, output types, limits, engines and verification methods.

Domain Object Operations
Integral transforms TransformProblem apply , solve , verify
Complex analysis ComplexFunction analytic_continuation , analyticity , argument_principle , classify_singularity , conformal_at , conformal_map , contour_integral , derivative , laurent_series , residue , singularities , zeros
Complex analysis Contour winding_number
Continuous probability Distribution cdf , characteristic_function , convolve , cross_entropy , entropy , expectation , kl_divergence , mean , mgf , mixture , moment , order_statistic , pdf , quantile , query , survival , truncate , variance , verify
Continuous probability JointDistribution bayes , condition , correlation , covariance , marginal , order_statistic , verify
Continuous probability ConditionalDistribution , RandomVariable conditional cdf / mean / pdf / variance / verify ; random-variable transform
Exact graphs Graph , MultiGraph bfs , centrality , coloring , connected_components , cycle_detection , dfs , euler_path , matching , shortest_path , verify ; Graph also has isomorphic_to
Exact graphs DirectedGraph bfs , centrality , cycle_detection , dfs , shortest_path , strongly_connected_components , topological_sort , verify
Exact graphs WeightedGraph bfs , centrality , coloring , connected_components , cycle_detection , dfs , euler_path , matching , maximum_flow , minimum_cut , minimum_spanning_tree , shortest_path , strongly_connected_components , topological_sort , verify
Combinatorics CombinatorialClass , GeneratingFunction class count / generate / verify ; generating-function coefficient / recurrence / verify
Finite groups FiniteGroup center , centralizer , closure , commutator_subgroup , conjugacy_classes , cosets , generated_subgroup , normality , orbits , order , quotient , stabilizers , subgroups , verify
Finite groups PermutationGroup contains , orbits , order , stabilizer_chain , stabilizers , verify
Finite groups FiniteAbelianGroup , GroupHomomorphism abelian order / verify ; homomorphism image / kernel / verify
Finite algebra FiniteRing , FiniteField add , inverse , multiply , verify
Finite algebra Module abelian_group , hermite_normal_form , smith_normal_form , verify
Signals ContinuousSignal , DiscreteSignal continuous sample ; discrete autocorrelation , convolution , correlation , cross_spectrum , dft , resample , stft , window
Signals Spectrum , Filter , FilterDesign , FilterState spectrum idft ; filter apply_signal / initial_state / to_transfer_function ; design design ; state process
Control TransferFunction bode , feedback , frequency_response , impulse_response , nyquist , poles , root_locus , series , stability , step_response , to_filter , to_state_space , to_zero_pole_gain , zeros
Control StateSpaceSystem bode , coefficient_units , controllability , discretize , finite_lqr , frequency_response , kalman , kalman_state , lqg , lqr , mpc , nyquist , observability , observer , place_poles , poles , stability , state_feedback , to_discrete_control , to_transfer_function , zeros
Control DiscreteControlSystem bode , controllability , frequency_response , nyquist , observability , poles , stability , to_state_space , to_transfer_function , zeros
Control ZeroPoleGain , TransferMatrix ZPK bode / nyquist / poles / to_transfer_function / zeros ; matrix entry
Sequential control FiniteHorizonLQR , KalmanState , MPCPlan LQR control / rollout / verify ; Kalman predict / update ; MPC first_control / verify
Optimization OptimizationProblem certify_milp , solve , to_conic , verify_certificate , verify_milp_certificate
Optimization ConicProblem , QuadraticallyConstrainedProblem solve , verify_certificate
Differential geometry Metric inverse_metric , christoffel , riemann , ricci , scalar_curvature , einstein , geodesic_equations
Differential geometry CoordinateMap jacobian , verify
Differential geometry TensorField covariant_derivative , lie_derivative
Differential geometry DifferentialForm wedge , exterior_derivative , interior_product , pullback , hodge_star
Computational geometry Point distance_to
Computational geometry PointSet orientation , incircle , segment_intersection , convex_hull , nearest_neighbor , delaunay , voronoi
Computational geometry Polygon verify , contains , intersection , triangulate
Computational geometry Polytope verify , contains
Computational geometry Triangulation verify , to_simplicial_complex
Algebraic topology SimplicialComplex , CubicalComplex verify , chain_complex , boundary_matrix , homology
Algebraic topology ChainComplex verify , boundary_matrix , homology , euler_characteristic
Statistical evidence and inference StatisticalSample describe , covariance , empirical_distribution , evidence_profile , mann_whitney , wilcoxon , kruskal_wallis , ks_2samp , spearman , kendall , permutation_test , bootstrap
Survival analysis SurvivalDataset verify , kaplan_meier
Survival analysis KaplanMeierEstimate verify , survival_at
Survival analysis CoxProportionalHazardsModel verify , fit
Survival analysis CoxPHFit verify , diagnostics , predict_partial_hazard
Time series TimeSeriesDataset verify , acf , pacf , stationarity_test
Time series TimeSeriesAnalysis verify
Time series TimeSeriesModel verify , fit
Time series TimeSeriesFit verify , diagnostics , forecast
Time series TimeSeriesForecast verify
Stochastic processes PoissonProcess verify , pmf , moments , increment_distribution
Stochastic processes WienerProcess verify , finite_dimensional , increment_distribution
Stochastic processes GaussianProcess verify , finite_dimensional , condition
Stochastic processes ContinuousTimeMarkovChain verify , transition_matrix , distribution , stationary_distribution
Stochastic process results FiniteDimensionalDistribution , GaussianProcessPosterior , CTMCTransition verify
Stochastic differential equations StochasticDifferentialEquation verify , simulate , convergence_study
SDE simulations SDESimulation verify , path , terminal_values
SDE convergence SDEConvergenceStudy verify
Generalized linear models GeneralizedLinearModel verify , fit
Generalized linear models GLMFit verify , diagnostics , predict
Non-parametric results NonparametricTestResult , ResamplingResult verify
Partial differential equations PDEProblem verify , classify , boundary_compatibility , derive_weak_form
PDE results PDEClassification , PDECompatibilityReport verify
Weak formulations WeakForm verify
Finite-element mesh FEMMesh verify , reference_element , finite_element_space
Reference element ReferenceElement verify , basis , quadrature
Finite-element results BasisFunctionSet , QuadratureRule , FiniteElementSpace verify
FEM algebra AssembledSystem verify , solve
FEM solution FEMSolution verify , estimate_error
FEM error estimate FEMErrorEstimate verify , mark , compare
Refinement RefinementMarking verify , refine
Refined mesh RefinedMesh verify , reference_element , finite_element_space
Mesh transfer / convergence MeshTransfer , FEMConvergenceObservation verify

Source objects use the same boundary: transform/complex/probability objects, graphs and combinatorial structures, finite groups/rings/fields/modules, signals/filters/control systems, optimization problems, and Manifold Chart Metric / CoordinateMap / TensorField / DifferentialForm , plus Point / PointSet / Polygon / Polytope / Triangulation , and finite SimplicialComplex / CubicalComplex /integral ChainComplex , and typed StatisticalSample observations, GeneralizedLinearModel specifications, and SurvivalDataset / CoxProportionalHazardsModel survival sources, plus TimeSeriesDataset / TimeSeriesModel ordered-time sources, and PoissonProcess / WienerProcess / GaussianProcess / ContinuousTimeMarkovChain process-law sources, StochasticDifferentialEquation Itô models, and structured PDEProblem equations/domains/conditions. NonparametricTestResult , ResamplingResult , KaplanMeierEstimate , GLMFit , and CoxPHFit are derived-only, source-linked records with deterministic exact, numerical, or seeded-stream replay. TimeSeriesAnalysis , TimeSeriesFit , and TimeSeriesForecast , FiniteDimensionalDistribution , GaussianProcessPosterior , and CTMCTransition follow the same output-only replay boundary. PDEClassification , PDECompatibilityReport , and WeakForm replay their principal-part, represented-trace, or complete weak-identity result from the source problem. FEMMesh links that weak form and an optional verified triangulation. ReferenceElement , BasisFunctionSet , QuadratureRule , and FiniteElementSpace are output-only with replayable single- or multi-source ancestry. AssembledSystem retains local and sparse global contributions plus its space/quadrature sources; output-only FEMSolution retains the exact assembled-system source and replayable solver diagnostics. G.5 output-only FEMErrorEstimate , RefinementMarking , RefinedMesh , MeshTransfer , and FEMConvergenceObservation records retain the complete solution-to-child-mesh chain, marking policy, parent/child cells, interpolation weights and empirical rate inputs. SDESimulation and SDEConvergenceStudy additionally replay their PCG64 streams and discretizations. Derived-only types cannot be forged through public input.

Installation

pip install mathkernel           # Python mathematical core
pip install 'mathkernel[mcp]'    # add the optional MCP transport

From a source checkout:

python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install -e .           # Python mathematical core
pip install -e '.[mcp]'  # add the optional MCP transport

Optional extras:

pip install -e '.[perf]'    # numba — JIT kernels (sieves, GF(2^m), FWHT, closure search)
pip install -e '.[cuda]'    # CuPy + all nvidia-*-cu12 runtime libraries (RTX-class GPU)
pip install -e '.[latex]'   # antlr4 runtime for math_parse_latex
pip install -e '.[dev]'     # pytest

Lean 4 + Mathlib is installed by default on first mathkernel-mcp start and via mathkernel-lean-setup ( elan + a pinned lake workspace). Skip with MATHKERNEL_SKIP_LEAN_INSTALL=1 (CI/wheel smoke).

GPU note: CuPy wheels ship no CUDA libraries. The cuda extra installs the matching nvidia-*-cu12 pip packages — without them, cuBLAS/NVRTC DLL loads fail even though import cupy succeeds. GPU availability is probed at runtime with a real matmul, so a broken stack degrades gracefully to CPU. Verify your stack with python scripts/gpu_smoke.py .

Quickstart — MCP server

The server speaks MCP over stdio (FastMCP 3) and ships core instructions to the client at initialize time: discover → parse → context → trust discipline → async jobs → provenance. 162 tools, all prefixed math_ .

Typical agent session:

math_capabilities                                   # discover surface, limits, engines
math_parse("x^2 - 3*x + 2 = 0")                     # -> expr_id
math_context_create(domains={"x": "real"})          # -> context_id
math_reason(expr_id, context_id, formal=true)       # solve + independently verify
math_derivation_trace(step_id)                      # full provenance on demand

Long-running sweeps are async:

math_job_submit("collatz", {"n_max": 14})  ->  math_job_status(job_id)  ->  math_job_result(job_id)

Quickstart — Python library

The MCP server is a thin transport layer; everything is available in-process:

from mathkernel import MathKernel

kernel = MathKernel()

# symbolic
r = kernel.parse("x^2 - 2 = 0")
sol = kernel.solve(r.data["expr_id"], "x")
assert sol.ok and sol.trust.value == "symbolic"

# exact GF(2^m) field arithmetic
f = kernel.gf2m_create(8, "1b")  # AES polynomial x^8 + x^4 + x^3 + x + 1 (hex reduction part)
kernel.gf2m_compute(f.data["field_id"], "mul", ["53", "ca"])

# finite dynamics: an explicit eight-state cyclic permutation
transition = [1, 2, 3, 4, 5, 6, 7, 0]
fs = kernel.finite_system_create("uniform", transition)
km = kernel.koopman_matrix(fs.data["system_id"], {"kind": "walsh", "r": 3})
vis = kernel.koopman_visibility(fs.data["system_id"], {"kind": "walsh", "r": 3})
# Exact zeros certify the requested modes in this declared finite model.

# closure relations (njit meet-in-the-middle)
kernel.closure_search("cyclic", m="97", weight_bound=10, multipliers=["1", "5"])

Standalone modules ( mathkernel.gf2m , mathkernel.koopman , mathkernel.relations , mathkernel.cumulants , mathkernel.finite_fourier , mathkernel.transforms , mathkernel.integral_transforms , mathkernel.complex_analysis , mathkernel.continuous_probability , mathkernel.integers , mathkernel.computational_geometry , mathkernel.algebraic_topology , mathkernel.collatz , mathkernel.cuboid ) are usable without the facade when you don't need derivation tracking.

Trust model

formal                  Lean certificate accepted by the Lean kernel
exact                   exact computation / checked claim-specific certificate
symbolic                symbolic engine agreement (e.g. SymPy residual checks)
interval_certified      rigorous enclosure (mpmath interval)
numeric_high_precision  arbitrary-precision numeric
numeric                 float evidence (incl. GPU fast paths)
empirical / heuristic / unknown

Overall trust is limited by the weakest evidence required to establish the claimed result — never the maximum trust emitted by any single node. Independent backend disagreement is preserved as an explicit conflict, not averaged away.

Every MathResult also carries an evidence_bundle with separate computation, proof, certificate, numerical, model and empirical evidence. claim_evidence retains those bundles per conclusion instead of flattening unlike claims into one score. The legacy trust field remains a conservative summary and is automatically capped by the evidence required for the result. A producer-supplied justified_trust is a ceiling, never an override; an unverified proof or certificate supports only unknown .

Semantic statuses distinguish proof or certification strength from mathematical outcomes such as does_not_exist , undefined , infeasible and unsupported . These distinctions survive MCP serialization, asynchronous job retrieval, derivation replay, visualization and multimodal artifact assembly.

The capability registry separates advertised trust levels from verification methods. Query it by domain, input/output type, operation, trust level, verification method or engine; capability records also identify their execution handler and meaningful cost dimensions. Expression plans record the resolved capability route before the existing obligation executor runs it.

Exact and numeric paths are strictly separated: koopman/finite-dynamics tools default to exact=true (proof-grade rational/cyclotomic values); exact=false selects the vectorized numeric path (CuPy GPU when usable) and downgrades trust to numeric .

Decimal literals are approximate observations. A decimal ( RealNode ) anywhere in an expression caps its trust at numeric from parse onward — 0.1 + x parses as numeric , 1/2 + x as symbolic . Formal certificates (Lean) and exact SMT counterexamples are refused for approximate inputs, because the backends would encode decimal syntax as exact rationals — silently proving a different statement. Use exact rationals or interval certification when proof-grade evidence is needed.

Continuous symbolic mathematics

Continuous domains use typed objects and the compositional object_create apply model rather than exposing a flat CAS surface. Every operation records a four-obligation DAG: typed-input validation, candidate computation, domain-invariant verification and conservative evidence reconciliation.

  • Integral transforms — Laplace, Fourier, Mellin and bilateral Z transforms with explicit conventions, assumptions and regions of convergence. Inverse Z uses annulus-aware Laurent/residue extraction when justified. Verification records round-trip, linearity, convolution, differentiation, value-theorem and ROC obligations separately; unresolved obligations remain unknown .
  • Complex analysis — derivatives, analyticity candidates, zeros, singularities, Laurent series, residues, contour integration, winding numbers, argument-principle accounting, conservative identity continuation and domain-aware conformal maps. Branch conventions, cuts, excluded points, contour orientation and boundary incidents remain explicit.
  • Continuous probability — typed univariate, random-variable, joint and conditional distributions; PDF/CDF/survival/quantile, moments, transforms, entropy, truncation, convolution, mixtures, divergence, marginals, conditioning/Bayes, covariance/correlation and order statistics. Support, parameter constraints, Jacobians and inverse branches are retained.

Symbolic availability is candidate evidence, not independent proof. Same-engine identities are capped at symbolic ; decimal ancestry remains capped at numeric . does_not_exist (for example, a Cauchy mean) is distinct from an unsupported method or an unresolved convergence question.

Conventions and assumptions are part of the object. Fourier sign and normalization, transform source/target variables, complex branches/cuts, probability supports and parameter constraints are never selected silently. Contour orientation and singularity accounting are mandatory where the theorem depends on them.

Verification is operation-specific. Transforms retain every checked or unresolved identity and ROC obligation. Residues are compared with defining limit/derivative or Laurent-coefficient formulas; contour claims retain enclosed singularities, cuts and winding numbers. Probability verifies normalization, support-aware nonnegativity, CDF boundaries/derivative/monotonicity when decidable, and Jacobian branches. These are symbolic checks unless an exact certificate or separate numerical record says otherwise.

Failures use semantic statuses: candidate , unknown , unsupported , does_not_exist , and error are distinct. Known limitations include non-product joint supports, continuation without an explicit overlapping source domain, branch-sensitive argument-principle inputs, transforms whose ROC SymPy cannot establish, and general multivariate changes of variables without supplied inverse branches/Jacobians.

Continuous symbolic work is bounded by the global AST/output/solver-time limits and dedicated contour, joint-dimension, mixture-component, series-order, order-statistic and inverse-branch limits. Raise the corresponding MATHKERNEL_MAX_* value explicitly when a larger request is intentional.

# PDF → Laplace transform, preserving support and evidence ancestry
d = kernel.object_create("Distribution", {
    "family": "exponential", "parameters": ["2"], "variable": "x",
})
r = kernel.apply(d.data["object_id"], "integral_transform", {
    "transform": "laplace",
    "transform_variable": "s",
    "convention": "laplace_standard",
})
assert r.data["value"] == "2/(s + 2)"

Finite dynamics & PRNG analysis

A distinctive capability: exact spectral analysis of finite dynamical systems (X, μ, T, O) — built for (and validated on) PRNG structure analysis.

  • Koopman suite — transport matrix Q, observation-transfer C, mode visibility ρ_O, lagged state tensors (raw/connected), observed statistics, IPR/entropy diagnostics. Walsh bases for GF(2)^r, character bases for Z_M.
  • Stochastic observation transfer (library API) — exact FiniteJointLaw contractions for arbitrary finite latent joint laws; ordered Markov path moments/cumulants with the required multiplication operators; statewise multiplicativity-defect certificates; and exact finite-noise deterministic dilations for rational Markov kernels. The accompanying published primate quartet pilot deliberately records that the earlier K3ST split-zero diagnostic does not survive outside its group-based assumptions.
  • Branching General Markov tensors (library API) — exact FiniteMarkovTree sum-product laws and cumulants on heterogeneous rooted trees; exact L M R edge-flattening certificates with the sharp transition- rank bound; local stochastic observation channels as Kronecker transforms; exact left-inverse recovery, collision witnesses, collective sensor fusion, and channel-conditioned singular-value bounds. The published primate pilot distinguishes algebraic identifiability from finite-sample stability.
  • Statistical phylogenetic inference (library API) — probability-simplex projection; known-channel EM and constrained ridge recovery; held-out regularization selection; multinomial covariance and tangent-space Fisher information; nonnegative-rank multinomial likelihood; covariance-Wald rank diagnostics; and tie-safe quartet scoring. Controlled GM(4) experiments quantify the shared singular-value origin of visibility loss and inverse instability. Two fixed published-data pilots add site and moving-block bootstrap checks without claiming broad competitive accuracy.
  • Frozen phylogenetic benchmarking (library API) — FASTA, relaxed PHYLIP, practical NEXUS and Newick ingestion; portable source SHA-256 manifests; canonical protocol and corpus locks; result-blind quartet sampling from reference-tree splits; complete-case site provenance; site, circular-block, partition-stratified and whole-partition resampling; rank-tail, p-distance and normalized log-det baselines; and tie-safe corpus summaries. The bundled execution evaluates 22 predeclared correlated units from two published source alignments and a 1,920-alignment known-truth stress grid. A separate lock fixes the first 20 eligible BenchmarkAlignments datasets before acquisition; that external corpus is explicitly pending rather than silently replaced.
  • Observable connected-relation detection (library API) - exact and numerical pure-interaction laws; weighted conditional-expectation singular spectra; mode-specific stochastic visibility; exact local-channel transfer of connected amplitude; chi-square and null-Fisher information retention; exact invisibility certificates; finite necessary and constructive sufficient sample bounds; binary-parity scaling; and complementary sensor fusion. The controlled theorem shows that local visibility losses multiply in amplitude and square in information, yielding an s^(-2d) detection-cost law in the homogeneous binary specialization.
  • Relation-subspace visibility and sensor design (library API) - finite multi-parameter local relation laws; latent and observed Fisher Gram matrices; generalized retained-information eigenvalues and principal visibility directions; exact observation-blind collision certificates; direction-level information and sample multipliers; rank, E-optimal, trace, D-optimal and pseudo-logdet sensor-subset selection; efficient empirical partition transfer; and score-mean long-run-covariance correction. A frozen SkewDB adapter adds source/schema auditing, discovery/validation/challenge splits by held-out taxonomy, discovery-only preprocessing, source hashing and a fail-closed raw-data runner. The bundled SkewDB fixture is explicitly synthetic because the current full payload was not acquired in this environment.
  • Coordinate-invariant relation geometry (library API) - finite-simplex tangent vectors with the intrinsic Fisher metric; stochastic tangent pushforward; coordinate-invariant generalized retained-information eigenvalues; exact score/tangent equivalence; exact local chi-square transfer; worst-direction minimax necessary sample bounds; finite Bhattacharyya and retention-based pointwise sufficient counts; and iid, moving-block and cluster bootstrap intervals for ordered relation spectra. A SHA-256-locked local-resolution SkewDB adapter converts documented cumulative *_fit.csv tracks to window increments and explicitly separates genuine inputs from the bundled source-parameterized generated fixture.
  • Finite Fourier — exact arithmetic in ℚ(ζ_L) via cyclotomic polynomials: DFT over Z_M, output-transfer transforms, two-point difference coefficients, measure Fourier transforms, orbit corrections.
  • Closure search — short irreducible relations selected by the dynamics: cyclic ( Σ k_j·a^j ≡ 0 mod m ) and binary ( ⊕ (L^{jK})ᵀ w_j = 0 ), meet-in-the-middle with L1/Hamming weight bounds.
  • GF(2^m) from transitions — reconstruct the field (dual-orbit cyclic basis, minimal/reduction polynomial, Rabin-verified) purely from a generator's GF(2)-linear transition columns.
  • State-conditioned dynamics — exact per-state orbit access T^κ(x)(x) : least-lag solving, symmetry-to-access conversion, cocycle composition, exhaustive additive closure proofs, symbolic affine access maps, GF(2) baby-step/giant-step orbit solving, sparse giant-lag predictive closures, and constrained symmetry discovery where numeric probing only ranks candidates — canonical-rewrite or exhaustive proofs decide.

The scripts/ tree contains uniform, end-to-end reproductions for 25+ generators (xorshift/xoroshiro/xorwow families, MT19937, Melg19937, WELL19937a, MRG32k3a, PCG32/64(+fast), LXM, SplitMix64, SFC64, JSF64, Romu, Philox, Threefry, RXS-M-XS), each runnable from scratch with scripts/families/run_all.py and scripts/companion/run_all.py . Reference data ships in scripts/data/ — no external fixtures required.

Engineering mathematics

MathKernel provides typed engineering mathematics for signals, control systems and constrained optimization while preserving the same evidence and persistence contracts as the symbolic core.

Signals and spectra

Continuous and sampled signals carry explicit domains, sample grids and units. Spectral representations are typed rather than treated as anonymous arrays. FIR/IIR filters and filter designs retain coefficients, conventions and source signals, while immutable streaming state makes block-by-block processing replayable. Frequency-response and time-response operations record whether they used exact symbolic algebra or numerical evaluation.

Control systems

Typed SISO and MIMO models support state-space and transfer-function representations, continuous/discrete conversion, poles and zeros, stability checks, discretization, controller construction and observer construction. LQR, finite-horizon LQR, steady-state Kalman filtering, LQG composition and immutable Kalman prediction/update states retain plant/model ancestry and separate algebraic checks from modeling assumptions.

Constrained finite-horizon MPC keeps feasibility, optimality, terminal invariance, recursive-feasibility and stability claims separate. Frequency-domain analysis includes Bode, Nyquist and root-locus representations together with checked time responses.

Optimization and certificates

Linear and quadratic programs can return exact/checkable optimality witnesses where the supported fragment permits it. Infeasible LPs can expose Farkas certificates and unbounded problems can expose recession rays. MILP search results carry replayable proof trees rather than only an incumbent value. Conic and quadratic-constraint workflows support bounded SOCP/SDP product cones and Lagrangian-style certificates in their declared fragments.

External native candidate solvers are isolated in fresh processes with bounded requests and hard timeout termination. Candidate generation and certificate verification are distinct steps: a solver finding a point does not by itself establish a stronger claim than the verifier can check.

Geometry and topology

Differential geometry and tensor calculus

Immutable Manifold , Chart , and Metric objects feed typed GeometryTensor , Connection , and GeodesicSystem outputs. Metric operations compute inverse metrics, Christoffel symbols, Riemann/Ricci/scalar/Einstein curvature and affine geodesic equations. Exact symbolic checks cover inverse identities, torsion freedom, metric compatibility, Riemann symmetries, the first Bianchi identity and the contracted Bianchi identity. Chart domains and metric nondegeneracy conditions remain explicit.

Directional CoordinateMap objects carry explicit Jacobians and inverse-composition checks. Dense variance-aware TensorField objects and canonical sparse DifferentialForm objects support covariant and Lie derivatives, wedge products, exterior derivatives, interior products, pullbacks and Hodge stars. Checks include graded commutativity, d²=0 , pullback commutation with d , metric compatibility, coordinate-map composition and the Hodge double-star sign when metric signature is supplied. Orientation and signature are never guessed.

Computational geometry

Point , PointSet , Polygon , half-space Polytope , Triangulation , and derived VoronoiDiagram objects provide exact orientation, incircle and segment-intersection predicates, monotone-chain convex hulls, winding containment, exact squared-distance nearest neighbors, certified ear clipping, convex polygon clipping, empty-circumcircle Delaunay triangulation and finite Voronoi duals with explicit unbounded rays. Decimal predicates use conservative floating-point error filters; when topology cannot be established, the result is explicitly ambiguous rather than promoted to an exact classification.

Algebraic topology

Exact finite SimplicialComplex , CubicalComplex , and integral ChainComplex objects expand cells to canonical face closures and derive oriented boundary matrices. Complexes verify boundary[k-1] * boundary[k] = 0 before homology is attempted. homology computes free ranks and integer torsion over Z through certified Smith-kernel/quotient reductions, and exact Betti numbers plus representative cycles over Q or GF(p). boundary_matrix , chain_complex , and euler_characteristic expose ordered bases and the Euler–Poincaré cross-check.

Verified exact triangulations can be converted into canonical simplicial complexes and composed directly with homology operations; numeric or refuted triangulations cannot cross that exactness boundary. Closure expansion is bounded before combinatorial growth can exceed configured topology limits. Persistent homology, cohomology products and infinite/CW-complex inference are not claimed.

Statistics and stochastic modeling

Samples and descriptive statistics

StatisticalSample stores a rectangular nonempty matrix of finite concrete real observations, unique variable labels, optional unique observation IDs and explicit asserted sampling/population/design metadata. describe derives exact or ancestry-capped numeric moments and type-7 order statistics; covariance derives centered cross-products with sample or population normalization; empirical_distribution preserves exact frequency counts and rational probabilities; and evidence_profile audits the evidence boundary itself.

The required evidence establishes only calculations on the stored observations. Sampling metadata, empirical support and model assumptions stay in separate diagnostic evidence records, while population generalization and model validity remain explicitly unestablished. Missing values, unresolved symbolic observations and silent imputation are refused. Decimal input cannot upgrade, resource limits are checked before expensive work, and every derived object retains its source across persistence and restart.

Generalized linear models

Immutable GeneralizedLinearModel objects link to stored samples and produce derived-only GLMFit objects. Supported canonical pairs are Gaussian/identity, binomial/logit and Poisson/log. verify checks response domain, design rank and residual degrees of freedom; fit reports ordered coefficients, covariance/standard errors, fitted conditional means, deviance, null deviance, dispersion, convergence, score residual and conditioning. Fits independently support verify , diagnostics , and predict .

Exact-input Gaussian models use sufficient cross-products and exact normal equations. Numeric Gaussian fits use checked float64 least squares; logistic and Poisson fits use deterministic float64 IRLS. Rank deficiency, invalid or degenerate response domains, non-convergence, singular/ill-conditioned information and detected complete/quasi separation fail closed without a fit object. No ridge term, row deletion, imputation or family/link substitution is silent. Coefficient, covariance, deviance and prediction claims remain conditional on the stored sample/design; model validity, population generalization and causal effects are not inferred.

Nonparametric tests, permutation tests and bootstrap

Stored samples support mann_whitney , wilcoxon , kruskal_wallis , ks_2samp , spearman , and kendall , with explicit average ranks and tie corrections. method="auto" performs complete exact sign/label/permutation enumeration only when both state and work estimates fit configured bounds; otherwise the result names its normal, chi-square, Kolmogorov or Student-t approximation. Thus an exact p-value is an exact conditional null calculation for the stored observations, while an asymptotic p-value remains numerical evidence without a finite-sample error theorem.

permutation_test supports mean/median differences using exact enumeration or explicitly seeded PCG64 Monte Carlo with an add-one p-value. bootstrap supports mean/median percentile intervals with a mandatory uint64 seed, bounded draws and memory-bounded batches. Simulated results record random algorithm, seed, draw count and replay configuration. Exchangeability, sampling design, asymptotic validity, population coverage and causal interpretation remain separate assumptions or unestablished claims.

Survival analysis

SurvivalDataset stores durations, exact binary event indicators, optional delayed-entry times and optional strata inside an immutable statistical sample. kaplan_meier constructs exact risk sets and product-limit values together with numerical Greenwood standard errors and two-sided log-log intervals. Multi-stratum inputs require an explicit stratum, and survival_at queries the right-continuous step curve.

CoxProportionalHazardsModel provides an unstratified Cox surface with explicit Efron or Breslow ties. Its deterministic float64 Newton fit uses monotone line search and refuses rank-deficient, event-sparse, non-convergent, singular, over-conditioned or separation-like cases. CoxPHFit records coefficients/hazard ratios, covariance/standard errors, partial likelihood, score residual, baseline hazard, concordance and Schoenfeld time correlations, with replay verification, diagnostics and bounded partial-hazard prediction. Independent censoring, proportional hazards, population generalization and causality remain assumptions or unestablished.

Time-series models and forecasting

TimeSeriesDataset preserves row order, distinct time/value columns, strict timestamps, reject-missing policy and detected regular spacing. Exact-source acf uses a common lag-zero centered denominator and pacf uses Durbin–Levinson recursion. stationarity_test provides a numerical constant-case ADF regression with named asymptotic critical values rather than inventing an exact p-value or claiming stationarity is proved.

TimeSeriesModel covers AR, MA, ARMA, ARIMA and GARCH orders, constant choice, Gaussian innovations and initialization. ARMA-family fits use bounded conditional-sum-of-squares optimization; GARCH uses constrained Gaussian likelihood with positive variance and persistence below one. Derived fits record coefficients, residual/fitted series, conditional variance, roots, likelihood, AIC/BIC and convergence, with Ljung–Box/Jarque–Bera diagnostics. Forecasts derive regular future times, recursive means and Gaussian intervals using ARIMA impulse responses or GARCH variance recursion. Irregular spacing may be analyzed but not fitted.

Stochastic processes

Immutable PoissonProcess , WienerProcess , GaussianProcess , and ContinuousTimeMarkovChain objects expose finite-dimensional laws and checked derived artifacts. Poisson count masses/moments and Wiener means/covariances are symbolic or exact. Gaussian-process finite laws support RBF, Matérn-3/2, linear and Brownian kernels with numerical PSD checks; conditioning uses bounded float64 Cholesky solves, explicit observation-noise variance and optional stored jitter without silently fitting hyperparameters. CTMC verification checks generator and initial-law axioms exactly; transitions use a checked matrix exponential, while stationary laws use an exact left-nullspace system and preserve nonuniqueness.

Independent/stationary increments, continuity, Gaussianity, kernel suitability and time homogeneity remain declared model assumptions rather than facts established by calculation.

Stochastic differential equations

StochasticDifferentialEquation supports vector Itô systems with declared symbol scope, drift vector, full state-by-noise diffusion matrix, concrete initial state and finite interval. Euler–Maruyama supports vector states and full diffusion. Milstein is restricted to scalar state/scalar noise and uses the symbolic diffusion derivative; unsupported multidimensional cases are refused rather than silently substituting another scheme.

Simulation records the exact step grid when possible, float64 paths, PCG64 algorithm/seed/stream, terminal sample moments and nominal strong/weak orders. Large outputs expose compact metadata plus bounded path/terminal queries. Coupled convergence studies reuse a finest Brownian stream across multiple step sizes and report observed terminal RMS convergence when defined. Simulation and convergence remain numerical/empirical; nominal orders, existence, uniqueness and regularity are assumptions, not proofs.

Statistical evidence and persistence

Across all statistical/stochastic objects, exact, symbolic, asymptotic, numerical, empirical and model evidence remain distinct. Derived types are output-only, replay operates under current limits, decimal ancestry cannot upgrade, persisted JSON is integrity checked before decoding, and stored type/class/source fields are reconciled to prevent cross-type source substitution.

PDEs and adaptive finite elements

PDE representation and classification

Typed PDE problems support scalar and coupled systems, declared independent/dependent variables, derivative multi-indices, coefficients/parameters and explicit initial/boundary conditions. Principal-part analysis classifies the represented system only within the declared symbolic fragment, and trace compatibility checks distinguish represented boundary information from stronger claims such as existence, uniqueness, regularity or well-posedness.

Weak forms

PDEFunctionSpace , PDEMeasure , WeakIntegralTerm , IntegrationByPartsStep , and output-only WeakForm artifacts represent weak formulations explicitly. derive_weak_form requires integration variables, ordered trial spaces, test spaces, boundary-trace indices and selected term/coordinate transfers; it does not guess analytic spaces or silently integrate terms.

Variable-coefficient integration by parts retains the complete product rule, storing differentiated-test and coefficient-derivative volume terms separately. Every transfer emits oriented boundary faces. Boundary terms that vanish under declared zero test traces remain represented and are marked as such. Dirichlet, Neumann/Robin and periodic indices are recorded as essential, natural and periodic partitions. WeakForm.verify reconstructs spaces, measures, volume/boundary terms, signs, product-rule derivatives, partitions and derivation steps from the source PDE. The verified claim is the represented integral identity under declared assumptions—not a theorem of solvability or regularity.

Meshes, reference elements and finite-element spaces

FEMMesh supports interval, triangle and tetrahedron simplices. Construction checks bounded connectivity, nondegeneracy, canonical positive orientation, boundary/interior facet incidence, induced boundary ownership and cell connected components. A compatible stored Triangulation can provide triangle connectivity while preserving geometry and weak-form ancestry. Combinatorial replay does not infer geometric non-overlap or approximation quality.

reference_element provides canonical unit simplices. basis derives symbolic nodal P1 Lagrange functions and gradients and checks the Kronecker property, partition of unity and gradient sum. quadrature supplies bounded exact-moment rules for the supported simplex degrees. finite_element_space builds P1 vertex-DOF C0 spaces with explicit local-to-global connectivity and essential boundary DOFs. Derived objects are replayable and output-only.

Assembly and algebraic solves

AssembledSystem and FEMSolution support scalar linear stationary weak forms on affine P1 simplices. Assembly stores dense local matrices/vectors and Jacobian determinants, coalesces the global matrix into ordered sparse entries, integrates supported Neumann/Robin facet terms and performs documented symmetric elimination for Dirichlet DOFs while retaining raw and transformed systems. Concrete substitutions resolve remaining PDE parameters through restricted MathIR.

Assembly distinguishes exact integration from an exact finite quadrature sum. Insufficient-order or non-polynomial quadrature may still define a replayable algebraic system, but quadrature_exact=false records the limitation. Unsupported strong second derivatives, time derivatives, coupled/nonlinear fields, periodic constraints, unresolved parameters and missing boundary fluxes fail closed.

Solves select exact rank/augmented-rank analysis or an explicit SciPy sparse numeric path. FEMSolution records unique , ill_conditioned , singular_inconsistent , singular_underdetermined , or singular_least_squares , together with residual and conditioning diagnostics. Verification establishes the transformed finite-dimensional system and solver outcome only, never a continuous PDE solution theorem or continuum error bound.

Error estimation and adaptivity

FEMSolution.estimate_error provides residual–jump indicators for complete unique or ill-conditioned P1 solutions in its supported scalar stationary diffusion fragment. Each CellErrorIndicator retains diameter-weighted strong residual, interior conormal-jump contribution, natural-boundary contribution and total. FEMErrorEstimate stores local/global estimator values, quadrature-exactness and algebraic residual separately, and always records rigorous_error_bound=false ; reliability and efficiency constants are not inferred.

FEMErrorEstimate.mark implements deterministic Dörfler and maximum policies. RefinementMarking.refine applies triangle red refinement and propagates conforming closure through shared edges. RefinedMesh records requested/closure cells and child-to-parent mappings; MeshTransfer records refined P1 nodal values as explicit affine combinations of parent DOFs. Refined meshes can re-enter the basis, quadrature, space, assembly, solve and estimation chain.

FEMErrorEstimate.compare accepts direct parent/child refinement pairs and reports estimator ratios and observed two-mesh rates. FEMConvergenceObservation is explicitly empirical evidence about an estimator sequence, not a convergence theorem or continuum error bound.

Relation and information-geometry inference

Composite relation inference

mathkernel.composite_relation_inference provides a quadratic score test for an entire visible relation subspace. Generalized observed scores are whitened under the nominal law and the statistic is the squared norm of their sample mean. A finite bounded-score argument supplies a conservative guarantee with explicit dependence on relation dimension, weakest retained-information eigenvalue, perturbation radius and score bound.

The same module computes nuisance-adjusted target information through latent and observed Fisher Schur complements. It reports exact post-observation confounding when a target direction can be reproduced by nuisance variation. For repeated or nearly repeated information eigenvalues, bootstrap uncertainty is attached to invariant eigenspaces through principal angles rather than arbitrary individual eigenvectors. Studentized ordered-spectrum intervals, dependence-informed circular-block heuristics, nominal/empirical/HAC covariance modes and norm-bounded misspecification guarantees are available with their assumptions recorded.

Robust relation inference

mathkernel.robust_relation_inference provides model-scoped quadratic inference, learned nuisance projections, orthogonal residual relations and VAR-prewhitened long-run covariance estimation.

Python API Function and evidence boundary
quadratic_minimax_bounds Gaussian-sequence lower/upper rates using the inverse information spectrum; separate finite iid U-statistic bound under a justified covariance envelope
gaussian_quadratic_test Weighted-square test with finite Gaussian Chernoff threshold
quadratic_u_test O(Nr) unbiased pair statistic; finite Cantelli calibration for iid known-null scores
prewhitened_long_run_covariance VAR(1), automatic Bartlett bandwidth, recoloring and persistence diagnostics; consistency assumptions remain necessary
quadratic_moment_test Full-rank asymptotic Wald test with empirical or supplied covariance; singular covariance is rejected
relation_folds Reproducible iid, group-preserving or contiguous folds
crossfit_nuisance_projection Out-of-fold nuisance-projection estimation in a declared candidate span
crossfit_residual_relations Orthogonal residual cross-moments with learned conditional means, custom learners and exclusion gaps

These research APIs remain numerical/model-scoped unless a stronger finite guarantee is explicitly returned. They do not acquire formal-proof or interval-certification labels merely because they are composed with other MathKernel objects.

Relation visibility, sensor design and information geometry

The relation-analysis stack also includes exact observable-relation visibility, information-retention calculations, sample-cost diagnostics, multi-relation Fisher geometry, sensor-design objectives, coordinate-invariant tangent representations, local testing bounds and uncertainty for information spectra. Numerical near-null directions are kept distinct from mathematically exact blind directions.

Performance: numba · CUDA · parallelism

Workload CPU fast path GPU path Parallel
Collatz sieve njit (n ≤ 31) CUDA RawKernel persistent process pool
Cuboid sweep njit leg-pair scan + QR prefilter CUDA RawKernel process pool
GF(2^m) ≤ 1024 njit n-limb (uint64×N) kernels
Integer batch njit array kernels persistent process pool, adaptive chunksize
Graph BFS/components njit CSR traversal, certificate re-verified
GF(p^m), p < 2^24, m ≤ 64 njit uint64 polynomial mul/mod
Cayley-table validation njit axiom scan
Recurrence extension checked int64 njit, bigint fallback
FWHT int64 njit butterfly
Closure search njit MITM (int64/uint64)
Koopman / finite dynamics numpy complex128 CuPy matmul
Obligation DAG thread waves
Long sweeps async job pool

Exact symbolic types ( Fraction , CyclotomicNumber ) are deliberately pure Python — a visibility zero or closure cancellation must remain a proof . Numeric twins exist where scale demands it and always carry trust: numeric .

Expansion contract. New domains must design verification and performance tiers together from the start: exact typed semantics and limits, an independently checkable certificate for every VERIFIED claim, and — where the workload is regular enough — a Numba/process/GPU fast path behind a narrow exactness fragment with automatic Python fallback. Fast paths must be re-verified or differential-tested against the reference implementation and must record the selected backend in evidence metadata; they may never raise trust beyond the underlying proof. GPU offload is mandatory only for regular device-exact workloads; irregular arbitrary-precision algorithms document the considered tiers instead.

Correctness-preserving optimization

MathKernel optimizes only where the mathematical contract survives the optimization. Regular bounded integer/array workloads use Numba, process or GPU paths with differential checks and guarded fallbacks. Exact symbolic workloads stay on exact representations when converting them to floating point would weaken the claim. Profiling is used to remove repeated symbolic work, hoist invariant computations, cache replayable certificates and replace avoidable superlinear verification passes without changing stored mathematical evidence. Backend selection is recorded in evidence metadata and never raises trust above the underlying computation or certificate.

Visualization & portable artifacts

mathkernel_viz turns MathKernel objects and results into evidence-carrying interactive artifacts. Visualization is downstream of mathematics: it consumes typed source data or a MultimodalProjection , records presentation transformations, and never upgrades the source evidence merely because a particular graphical form is used.

import mathkernel_projection as mkp
import mathkernel_viz as viz

projection = mkp.create_projection(
    "matrix",
    {"matrix": [[1, 2], [3, 4]]},
    trust="exact",
)
doc = viz.from_projection(projection)
viz.export_html(doc, "matrix.html", mode="portable")

The lower-level dashboard API remains available for direct composition:

import mathkernel_viz as viz

doc = viz.dashboard("My result", cols=2)
viz.add_point_cloud(doc, points, trust="numeric")
viz.add_histogram(doc, values, bins=128)
viz.add_select(doc, "lag", [
    {"label": "k=4", "value": {"embed": {"lags": [0, 4, 8]}}}
])
viz.export_html(doc, "out.html", mode="portable")
  • Building blocks, not monoliths — artifacts compose reusable panels such as point_cloud_3d , trajectory_3d , surface_3d , vector_field_3d , plot2d , histogram , heatmap , dag , metric_grid , data_table , text and select .
  • Renderer-neutral IR — the versioned VisualizationDocument is consumed by pure-Python SVG, optional matplotlib PNG/PDF, and the HTML+Three.js renderer.
  • Interactive 3D — orbit/pan/zoom and hover inspection of identity and trust.
  • Portable HTML — one self-contained .html with embedded datasets, provenance, reproducibility metadata and viewer runtime; no server or CDN is required.
  • Evidence-preserving — block/series/dataset trust is inherited conservatively; interval-certified display is only used when the source itself carries that support.
  • Integrity & determinism — payload and per-dataset SHA-256 are exposed, and identical inputs produce deterministic artifacts.
  • Secure presentation boundary — CSP, escaped labels, no eval , dataset limits, and MathIR treated as data rather than executable code.

Shared multimodal projections

The shared mathkernel_projection layer defines canonical mathematical projection families that can feed visualization, sonification, or a combined research artifact. This prevents each renderer from inventing its own interpretation of a matrix, mesh, graph, field, distribution or high-dimensional object.

A MultimodalProjection records:

  • source lineage ( SourceRef );
  • projection family and structured payload;
  • coordinates, units and labels;
  • assumptions and evidence references;
  • deterministic transformation provenance;
  • explicit basis, slice, traversal or ordering parameters;
  • output dimensionality and declared information loss.

The canonical families cover scalar/vector fields; point sets/clouds; curves, surfaces and trajectories; sequences and distributions; matrices and tensors; graphs, evidence graphs, expression trees and certificate trees; spectra and complex-valued fields; regions and implicit sets; meshes and geometric complexes; ODE/PDE solutions and dynamical systems; optimization and statistical-inference objects; finite-field/GF(2) structures; relation/information geometry; sets, partitions and piecewise objects; quantities with units; ensembles; and explicit higher-dimensional projections.

For source dimension greater than three, a projection method and output dimensionality must be explicit. Coordinate selection, a declared basis, PCA-like reduction or a domain-specific spectral projection are transformations that must be recorded; a renderer cannot silently decide which view is canonical.

A registry of result adapters ( mathkernel_projection.result_adapters ) maps stored typed objects and flat result payloads onto these families automatically. Adapters are pure extraction functions: they never recompute mathematics, never upgrade trust, and declare any presentation choice (sampling grids, magnitude-only spectra, channel selection, covariance-to-band reduction) in parameters and information_loss . math_visualize(object_id=...) and math_projection_create(source_object_id=...) use the registry to choose the canonical projection for signals, spectra, filters, pole-zero maps, frequency responses, root loci, time responses, distributions (symbolic densities are sampled on a declared window), empirical/discrete distributions, statistical samples, GLM fits, Kaplan-Meier estimates, Cox baseline hazards, ACF/PACF diagnostics, time-series fits, graphs and traversal trees, optimization results, ODE/SDE ensembles, FEM meshes/solutions/error indicators/convergence observations, assembled-system sparsity patterns, PDE grids, point sets, polygons, triangulations, Voronoi diagrams, generating functions, Cayley tables, contours, singularity maps, subgroup/coset/orbit partitions, combinatorial counts, and unit quantities. Unregistered object types fail with a typed error rather than an invented view.

Evidence graphs are first-class: claim -> evidence -> assumption/source relationships can be visualized directly, making MathKernel's verification structure inspectable rather than hiding it in metadata. Complex-valued projections retain magnitude/phase structure, and mesh/field projections preserve the geometric entity to which each value belongs.

Artifact lineage and scientific presentation

mathkernel_viz , mathkernel_sonify and mathkernel_multimodal share the mathkernel_artifacts semantic layer. MathKernelArtifact carries typed source lineage, evidence/certificates, presentation transformations, scientific/perceptual annotations, reproducibility metadata and visual/audio synchronization. mathkernel_viz.visualize(result) attaches deterministic structured lineage to visual datasets and series, while mathkernel_viz.to_artifact(doc, result=...) promotes a visual document into the same evidence-carrying artifact model used by multimodal exports. Presentation remains downstream of mathematics and cannot upgrade source trust.

Scientific sonification ( mathkernel-sonify )

mathkernel_sonify is the auditory sibling of mathkernel_viz . It consumes the same source lineage and MultimodalProjection contract, while SonificationDocument owns the auditory mapping itself. The mathematical result remains untouched.

import mathkernel_projection as mkp
import mathkernel_sonify as son

projection = mkp.create_projection(
    "spectrum",
    {"amplitudes": [1.0, 0.42, 0.17], "phases": [0.0, 0.3, -0.2]},
    trust="numeric",
)
audio = son.projection_sonification(projection)
son.write_wav(audio, "spectrum.wav")
son.export_html(audio, "spectrum.html")

The IR records every value-to-audio mapping as declarative provenance. Structured objects are never silently flattened: matrix scans record row/column ordering; tensor sonification records the selected slice/order; graphs record traversal or degree reduction; meshes record the geometric reduction; complex objects preserve magnitude and phase mapping; optimization traces, bootstrap/null distributions, relation spectra and ensemble orderings are likewise explicit.

Built-in adapters cover harmonic/Fourier additive synthesis, sequential scans, prediction-vs-observation stereo comparison, residual sonification and projection-aware structured mappings. Offline PCM/WAV rendering is deterministic, rejects silent Nyquist aliasing, and applies explicit normalization/peak limits. The WebAudio exporter is a single offline HTML file with no network dependency.

Scientific rule: an audible pattern is a perceptual candidate, not mathematical evidence. Any pattern discovered by listening must be validated quantitatively, exactly, formally or empirically through MathKernel.

Unified multimodal artifacts ( mathkernel-multimodal )

mathkernel_multimodal combines visualization and sonification derived from the same source/projection into one portable MathKernelArtifact . Shared SourceRef ancestry allows automatic cross-modal synchronization without weakening the mathematical trust model.

import mathkernel_multimodal as mkm

artifact = mkm.build_artifact(
    title="Result",
    visualizations=[viz_doc],
    sonifications=[son_doc],
    mathkernel_version="current",
)
mkm.export_html(artifact, "result.html")
  • visual blocks can highlight during linked audio playback and linked audio can seek from a visual block;
  • one inspector surface exposes Result, Evidence, Provenance, Data, Reproduction, Visual Mapping, Audio Mapping, Sync and Annotations;
  • payload verification and document integrity hashes remain available in the exported artifact;
  • portable output works from file:// , with no running MathKernel server required;
  • artifact trust remains the weakest justified source/member trust.

Via MCP, research artifacts can be assembled from stored visualization and sonification objects and exported as a single self-contained file.

MCP tool surface

All 167 tools (click to expand)
Group Tools
Discovery math_capabilities , math_capability_query , math_result_resource_get
Typed mathematics math_object_create , math_object_get , math_apply — complete compositional surface tabulated above, including geometry, signals/control, certified optimization, statistics/stochastic systems and general PDE representation
Parsing math_parse , math_parse_latex , math_get , math_substitute , math_infer_structure
Algebra math_simplify , math_solve , math_solve_system
Calculus math_differentiate , math_integrate , math_limit , math_series , math_summation , math_product
Numeric math_numeric_evaluate , math_interval_evaluate
Matrices math_matrix_create , math_matrix_get , math_matrix_det , math_matrix_inverse , math_matrix_transpose , math_matrix_multiply , math_matrix_rank , math_matrix_rref , math_matrix_eigenvalues , math_matrix_solve
Context math_context_create , math_context_infer , math_context_check
Reasoning math_analyze , math_plan , math_plan_get , math_execute_plan , math_reason , math_execution_get , math_prove_equivalence , math_counterexample
Codegen math_codegen , math_verify_code , math_execute_code
Integers math_integer_analyze , math_integer_compute , math_integer_batch
Sweeps math_collatz_sieve , math_cuboid_sweep
Jobs math_job_submit , math_job_status , math_job_result , math_job_list
GF(2^m) math_gf2m_create , math_gf2m_from_transition , math_gf2m_compute , math_gf2m_coords , math_gf2m_root_jump_rows , math_gf2m_closure_roots , math_gf2m_jump_rows
GF(2) math_gf2_rank , math_gf2_nullspace , math_gf2_carryfree_cols , math_gf2_minpoly
Transforms math_fwht
Finite dynamics math_finite_system_create , math_koopman_matrix , math_koopman_transfer , math_koopman_visibility , math_koopman_lagged , math_koopman_observed , math_koopman_diagnostics , math_finite_fourier_compute , math_closure_search , math_cumulant_compute
Conditioned dynamics math_conditioned_access_solve , math_conditioned_symmetry_access , math_conditioned_access_compose , math_conditioned_closure , math_symbolic_conditioned_access , math_affine_conditioned_access , math_gf2_conditioned_access , math_gf2_predictive_closure , math_synthesize_conditioned_closures , math_synthesize_gf2_vector_conditioned_access , math_discover_structural_conditioned_closure , math_discover_factor_swap_conditioned_closure
Multimodal projections math_projection_catalog , math_projection_create , math_projection_describe
Visualization math_visualize , math_visualize_dag , math_render_koopman , math_visualize_projection , math_export_artifact
Sonification math_sonify , math_sonify_compare , math_sonification_describe , math_sonify_projection , math_export_audio
Multimodal artifacts math_research_artifact_create , math_export_research_artifact
Sets & logic math_set_create , math_set_op , math_set_membership , math_quantifier_check , math_quantifier_eliminate , math_quantifier_eliminate_batch
Polynomials math_poly_groebner , math_poly_divide , math_poly_resultant , math_poly_discriminant , math_poly_factor , math_ideal_membership , math_poly_groebner_batch
Probability math_prob_rv_create , math_prob_expectation , math_prob_variance , math_prob_covariance , math_prob_bayes , math_prob_markov_stationary , math_prob_markov_hitting_time , math_prob_sample , math_prob_distribution
Statistics math_stats_moments , math_stats_order , math_stats_regression , math_stats_correlation , math_stats_ttest , math_stats_chi2 , math_stats_confidence_interval , math_stats_batch_moments
Tensors math_tensor_create , math_tensor_get , math_tensor_contract , math_tensor_solve
Numerics math_root_find , math_root_scan , math_quadrature
ODE/PDE math_ode_solve , math_ode_solve_numeric , math_ode_ensemble , math_pde_heat_1d , math_pde_heat_2d , math_pde_wave_1d , math_pde_advect_1d , math_pde_ensemble , math_pde_mol_heat
Optimization math_optimize_critical_points , math_optimize_kkt , math_lp_solve , math_optimize_minimize , math_optimize_multistart
Units math_unit_check , math_unit_convert , math_unit_simplify
Assurance math_store_status , math_replay , math_fuzz_differential , math_certified_enclose
Proving math_prove , math_prove_batch , math_prove_replay
Provenance math_derivation_get , math_derivation_trace

Every tool docstring is written LLM-facing: parameter formats, exact-vs-numeric semantics, limits, and follow-up hints are documented in-place.

Configuration

All settings are environment-driven with the MATHKERNEL_ prefix ( Settings.from_env() ), introspectable via math_capabilities :

Variable Default Purpose
MATHKERNEL_MAX_INPUT_LENGTH 100000 parser input cap
MATHKERNEL_MAX_OUTPUT_SIZE_BYTES 256000000 whole-response byte budget; oversized payloads are preserved as integrity-checked resources and returned by receipt
MATHKERNEL_SOLVER_TIMEOUT_SECONDS 30 symbolic operation budget using bounded cancellable subprocess workers
MATHKERNEL_ENABLE_EXECUTION false sandboxed codegen execution (opt-in)
MATHKERNEL_YOLO_MODE false unlocks math_yolo_settings to mutate live MATHKERNEL_* settings (typed coerce; default off)
MATHKERNEL_Z3_TIMEOUT_MS 10000 SMT budget (set on every Z3 solver instance)
MATHKERNEL_LEAN_BINARY / MATHKERNEL_LEAN_TIMEOUT_SECONDS lean / 90 Lean adapter (timeout passed to every lake env lean check)
MATHKERNEL_SKIP_LEAN_INSTALL unset skip the default Lean 4 + Mathlib download
MATHKERNEL_LEAN_CACHE platform cache elan + lake workspace root
MATHKERNEL_ENABLE_PARALLEL / MATHKERNEL_MAX_WORKERS true / cpu_count process & thread pools
MATHKERNEL_MAX_ITERATIONS 10000 iteration cap for simplex / Nelder-Mead
MATHKERNEL_TOLERANCE 1e-12 numeric convergence tolerance
MATHKERNEL_MAX_ODE_STEPS 100000 RK45 integration step cap
MATHKERNEL_STORE_PATH unset opt-in SQLite persistence for expressions/derivations + math_replay
MATHKERNEL_PROVE_PORTFOLIO_SIZE 3 SMT encodings raced per math_prove call
MATHKERNEL_MAX_PDE_GRID 1000000 PDE solver grid-cell cap
MATHKERNEL_MAX_PDE_FIELDS / MATHKERNEL_MAX_PDE_DIMENSIONS 16 / 8 typed PDE field and independent-variable caps
MATHKERNEL_MAX_PDE_EQUATIONS / MATHKERNEL_MAX_PDE_TERMS 32 / 1024 typed PDE system and total-term caps
MATHKERNEL_MAX_PDE_CONDITIONS 1024 total typed boundary/initial-condition cap
MATHKERNEL_MAX_PDE_DERIVATIVE_ORDER / MATHKERNEL_MAX_PDE_NONLINEAR_POWER 4 / 8 derivative and represented-power caps
MATHKERNEL_MAX_PDE_WORK 2000000 typed PDE construction/replay work cap
MATHKERNEL_MAX_PDE_SPACES / MATHKERNEL_MAX_PDE_SPACE_ORDER 64 / 8 weak-form space-count and regularity-order caps
MATHKERNEL_MAX_PDE_WEAK_TERMS / MATHKERNEL_MAX_PDE_IBP_STEPS 4096 / 256 derived integral-term and integration-by-parts caps
MATHKERNEL_MAX_PDE_WEAK_WORK 5000000 weak-form derivation/replay work cap
MATHKERNEL_MAX_FEM_POINTS / MATHKERNEL_MAX_FEM_CELLS 100000 / 200000 simplex mesh vertex/cell caps
MATHKERNEL_MAX_FEM_DOFS 200000 finite-element-space DOF cap
MATHKERNEL_MAX_FEM_WORK 20000000 finite-element construction/replay work cap
MATHKERNEL_MAX_FEM_ASSEMBLY_NNZ / MATHKERNEL_MAX_FEM_ASSEMBLY_WORK 2000000 / 50000000 sparse-entry and assembly-work caps
MATHKERNEL_MAX_FEM_EXACT_SOLVE_DOFS / MATHKERNEL_MAX_FEM_NUMERIC_SOLVE_DOFS 256 / 100000 exact dense-diagnostic and numeric sparse-solve caps
MATHKERNEL_MAX_FEM_ESTIMATOR_WORK / MATHKERNEL_MAX_FEM_REFINED_CELLS 50000000 / 500000 residual-indicator replay work and refined-output cell caps
MATHKERNEL_MAX_QE_VARIABLES 16 quantifier-elimination variable cap
MATHKERNEL_MAX_BATCH_JOBS 10000 integer batch cap
MATHKERNEL_MAX_MATRIX_DIM 128 matrix engine cap
MATHKERNEL_MAX_JOBS_RETAINED 100 async job retention
MATHKERNEL_MAX_MATH_OBJECTS 10000 retained typed-object cap
MATHKERNEL_MAX_CONTOUR_VERTICES 4096 contour complexity cap
MATHKERNEL_MAX_JOINT_DIMENSIONS 8 joint-distribution dimension cap
MATHKERNEL_MAX_DISTRIBUTION_COMPONENTS 256 mixture component cap
MATHKERNEL_MAX_SYMBOLIC_SERIES_ORDER 128 Laurent/classification order cap
MATHKERNEL_MAX_ORDER_STATISTIC_SAMPLE_SIZE 1024 symbolic order-statistic sample cap
MATHKERNEL_MAX_GRAPH_VERTICES / MATHKERNEL_MAX_GRAPH_EDGES 4096 / 65536 typed graph size caps
MATHKERNEL_MAX_COMBINATORIAL_ITEMS 10000 lazy combinatorial generation cap
MATHKERNEL_MAX_GROUP_ELEMENTS 4096 finite-group enumeration cap
MATHKERNEL_MAX_FIELD_DEGREE 64 GF(p^m) extension-degree cap
MATHKERNEL_MAX_NORMAL_FORM_DIM 128 Smith/Hermite matrix dimension cap
MATHKERNEL_MAX_INVERSE_BRANCHES 256 change-of-variable branch/Jacobian cap
MATHKERNEL_MAX_OBLIGATION_STEPS 128 maximum executable plan obligations
MATHKERNEL_MAX_FWHT_SIZE 2²⁰ FWHT length cap
MATHKERNEL_MAX_FINITE_STATES 4096 finite-system enumeration cap
MATHKERNEL_MAX_CUMULANT_ORDER 8 cumulant/connected-tensor order cap
MATHKERNEL_MAX_CLOSURE_RESULTS 10000 closure-search result cap
MATHKERNEL_MAX_GEOMETRY_DIMENSION 8 manifold/chart dimension cap
MATHKERNEL_MAX_GEOMETRY_RANK 6 dense tensor-field rank cap
MATHKERNEL_MAX_GEOMETRY_POINTS 10000 point/vertex count cap
MATHKERNEL_MAX_GEOMETRY_SIMPLICES 100000 halfspace/triangle count cap
MATHKERNEL_MAX_GEOMETRY_WORK 1000000 preflight symbolic geometry work cap
MATHKERNEL_MAX_TOPOLOGY_DIMENSION 16 maximum finite-complex degree/ambient dimension
MATHKERNEL_MAX_TOPOLOGY_CELLS 10000 total simplicial/cubical/chain-basis cell cap
MATHKERNEL_MAX_TOPOLOGY_MATRIX_ENTRIES 1000000 stored boundary-matrix entry cap
MATHKERNEL_MAX_TOPOLOGY_ENTRY_BITS 4096 integer boundary-entry bit-length cap
MATHKERNEL_MAX_TOPOLOGY_WORK 2000000 exact topology preflight work cap
MATHKERNEL_MAX_STATISTICAL_VARIABLES 256 typed sample column cap
MATHKERNEL_MAX_STATISTICAL_OBSERVATIONS 100000 typed sample row cap
MATHKERNEL_MAX_STATISTICAL_CELLS 1000000 typed sample rectangular cell cap
MATHKERNEL_MAX_STATISTICAL_WORK 2000000 descriptive/covariance preflight work cap
MATHKERNEL_MAX_GLM_PARAMETERS 64 fitted coefficient cap, including the intercept
MATHKERNEL_MAX_GLM_ITERATIONS 200 requested IRLS iteration cap
MATHKERNEL_MAX_GLM_PREDICTION_ROWS 100000 conditional-mean rows per prediction request
MATHKERNEL_MAX_GLM_WORK 20000000 GLM rank/matrix/iteration preflight work cap
MATHKERNEL_MAX_NONPARAMETRIC_GROUPS 64 selected Kruskal–Wallis group cap
MATHKERNEL_MAX_EXACT_RESAMPLING_STATES 100000 complete sign/label/permutation state cap
MATHKERNEL_MAX_RESAMPLES 1000000 Monte Carlo permutation/bootstrap draw cap
MATHKERNEL_MAX_RESAMPLING_BATCH_CELLS 1000000 generated cells per bootstrap batch
MATHKERNEL_MAX_RESAMPLING_WORK 20000000 rank/enumeration/resampling preflight work cap
MATHKERNEL_MAX_SURVIVAL_STRATA 64 distinct survival-stratum cap
MATHKERNEL_MAX_SURVIVAL_TIMELINE_POINTS 100000 selected Kaplan–Meier timeline cap
MATHKERNEL_MAX_COX_PARAMETERS 64 Cox predictor cap
MATHKERNEL_MAX_COX_ITERATIONS 200 requested Cox Newton-iteration cap
MATHKERNEL_MAX_COX_PREDICTION_ROWS 100000 partial-hazard prediction-row cap
MATHKERNEL_MAX_COX_INFORMATION_CONDITION 1000000000000 observed-information condition ceiling
MATHKERNEL_MAX_SURVIVAL_WORK 20000000 survival risk-set/matrix/iteration work cap
MATHKERNEL_MAX_TIME_SERIES_LAG 1000 ACF/PACF/diagnostic lag cap
MATHKERNEL_MAX_TIME_SERIES_DIFFERENCE 2 ARIMA differencing-order cap
MATHKERNEL_MAX_TIME_SERIES_PARAMETERS 32 AR/MA/GARCH dynamic-parameter cap
MATHKERNEL_MAX_TIME_SERIES_ITERATIONS 500 fit-optimizer iteration cap
MATHKERNEL_MAX_TIME_SERIES_FORECAST_STEPS 10000 forecast-horizon cap
MATHKERNEL_MAX_TIME_SERIES_WORK 50000000 analysis/fit/forecast work cap
MATHKERNEL_MAX_STOCHASTIC_STATES 256 CTMC state cap
MATHKERNEL_MAX_STOCHASTIC_TIME_POINTS 10000 finite-dimensional/prediction time cap
MATHKERNEL_MAX_GP_CONDITIONING_POINTS 2000 GP observation cap
MATHKERNEL_MAX_STOCHASTIC_MATRIX_ENTRIES 1000000 covariance/generator workspace cap
MATHKERNEL_MAX_GP_CONDITION_NUMBER 1000000000000 GP conditioning ceiling
MATHKERNEL_MAX_STOCHASTIC_WORK 50000000 factorization/exponential work cap
MATHKERNEL_MAX_SDE_STATE_DIMENSION 32 SDE state dimension cap
MATHKERNEL_MAX_SDE_NOISE_DIMENSION 32 Brownian driver dimension cap
MATHKERNEL_MAX_SDE_STEPS 1000000 simulation/convergence step cap
MATHKERNEL_MAX_SDE_PATHS 100000 simulation path cap
MATHKERNEL_MAX_SDE_SIMULATION_CELLS 5000000 stored-path/random-increment cell cap
MATHKERNEL_MAX_SDE_WORK 50000000 SDE update-work cap
MATHKERNEL_MAX_SDE_QUERY_VALUES 20000 path/terminal values returned per query

Repository layout

src/mathkernel/            core library, typed mathematics and kernel facade
src/mathkernel_mcp/        FastMCP server layer and public math_* tools
src/mathkernel_projection/ shared typed multimodal projection layer
src/mathkernel_viz/        visualization IR, viewers and portable renderers
src/mathkernel_sonify/     scientific sonification IR, PCM/WAV and WebAudio
src/mathkernel_artifacts/  shared evidence, lineage and synchronization schema
src/mathkernel_multimodal/ unified visual/audio research-artifact exporter
scripts/                   reproducibility, GPU checks and demonstrations
experiments/               research validation programs and datasets
skills/                    synchronized Python and MCP agent skills
tests/                     core, regression, multimodal and domain test suites
benchmarks/                correctness-gated performance measurements

Skill packages

MathKernel ships two synchronized agent-skill packages: one for direct Python use and one for MCP clients. They document the same evidence contract, object lifecycle and mathematical semantics, while adapting examples to their respective interfaces.

The skills cover symbolic/exact work, reasoning and proving, persistence, finite dynamics, probability/statistics, numerics, tensors/units, performance, visualization, scientific sonification and the shared multimodal projection workflow. The viz/audio skills now require projection-first provenance for structured objects and explicit high-dimensional reduction or acoustic extraction rather than hidden flattening.

Testing

Run the complete source-tree suite with the optional dependencies required by the domains you want to validate:

PYTHONPATH=src:. python -m pytest -q
python scripts/gpu_smoke.py

The repository degrades unavailable optional engines to unknown or unavailable rather than fabricating success. FastMCP is required for MCP registration tests, z3-solver for SMT/proving/quantifier-elimination tests, and the compatible ANTLR runtime for SymPy LaTeX parsing. Domain-specific test modules and experiment runners can be executed independently when validating a particular mathematical surface.

Coverage includes parser and ambiguity handling, symbolic algebra and calculus, exact integer and finite-field arithmetic, graph algorithms, linear algebra, Numba/CUDA differential paths, asynchronous jobs, code generation and checking, GF(2) and finite Fourier methods, Koopman/finite dynamics, PRNG analysis, typed engineering mathematics, geometry/topology, statistics and stochastic systems, PDE/FEM/adaptivity, evidence propagation, persistence integrity, visualization, sonification, multimodal artifacts and the MCP tool surface.

CI targets supported Python versions with native thread fan-out bounded per worker. Distribution checks build the sdist and wheel, verify metadata, install the wheel in a clean environment, confirm the runtime version and check that vendored offline visualization/multimodal assets are present. Portable exports therefore do not require a CDN after installation.

Safety boundaries

  • No raw user expression ever reaches sympify() / parse_expr() ; restricted grammar, unknown functions rejected, ambiguous notation refused with candidates.
  • Chunked arbitrary-length integer conversion; big-result output guards; bounded automatic number-theory work; obligation step ceilings; dependency/cycle validation.
  • Sandboxed code execution is opt-in ( MATHKERNEL_ENABLE_EXECUTION=1 ), runs in an isolated subprocess with a timeout, and is always labeled numeric evidence.
  • Lean subprocess invocation uses shell=False ; optional engines report unknown / unavailable rather than fabricating success.
  • External native LP/QP/MILP, conic/QCQP, Riccati/LQG and numerical pole-placement candidate searches run in fresh interpreters whose process groups are killed on timeout. Requests/results are bounded and BLAS/OpenMP fan-out is capped.
  • SQLite persistence checks every JSON payload with SHA-256 before decoding. canonical typed records additionally reconcile their declared object type, decoded model class, and source-link field before retrieval or execution. Corrupt or substituted records fail closed without producing derived objects.

This termination boundary is not a hostile-code sandbox and does not impose an OS memory quota. Multi-tenant isolation still belongs in an external worker or sandbox layer.

License

Copyright © 2026 Maarten Boone.

Released under the MIT License .

Hard-Chat – A serverless, RAM-only P2P terminal chat

Hacker News
github.com
2026-09-06 19:59:43
Comments...
Original Article

🔒 Zero-Trace Terminal

End-to-end encrypted P2P chat, right in your browser. No server, no accounts, no stored history.

by Hardlint Cybersecurity Team


⚠️ Important Notice

This project is distributed for educational and security research purposes . It does not guarantee network-level anonymity: it protects the content of conversations, not necessarily who is connecting. Read the Attack Surface and Known Limitations section before using it for sensitive communications.

Use of a trustworthy VPN on both devices is strongly recommended.


✨ Features

  • 🔐 End-to-end encryption — AES-GCM 256-bit, key derived via PBKDF2 (100,000 iterations)
  • 🌐 True P2P connection — direct WebRTC link between the two devices, no central server relaying messages
  • 🚫 Zero persistence — no cookies, no localStorage, no database: close the tab and nothing remains
  • 🔑 Single shared secret — a randomly generated Room Key (100 characters), no manual technical configuration required
  • 🧹 Panic Purge — one button instantly wipes keys, connection state, and visible chat history
  • 📡 Reliable connectivity — 18 STUN/TURN servers configured as fallbacks to work even behind restrictive NATs (4G/5G, corporate networks)

🚀 How to Use

  1. Open the page (must be served over HTTPS — e.g. via GitHub Pages, not opened as a local file)
  2. Host: click [1] INITIALIZE ROOM → copy the generated Room Key
  3. Send the Room Key to your contact through a different channel (in person, voice call, another encrypted app)
  4. Guest: click [2] CONNECT TO ROOM → paste the received Room Key
  5. Wait for the connection (usually a few seconds) → the chat opens
  6. If the connection isn't established within 2 minutes, the Room Key expires automatically: generate a new one with the dedicated button

📋 Requirements

  • Modern browser with WebRTC and Web Crypto API support (recent Chrome, Firefox, Edge, Safari)
  • Internet access on both devices
  • The page must be served over HTTPS (Secure Context is required for Web Crypto API and WebRTC) — it does not work when opened as a local file
  • Both parties must have the page open at the same time during the connection attempt
  • The Room Key must be copied in full, exactly 100 characters , with no extra spaces or line breaks

🏗️ Architectural Overview

Zero-Trace Terminal is a static web application (HTML/CSS/JS, no proprietary backend) that allows two devices to establish a direct peer-to-peer connection via WebRTC, exchanging end-to-end encrypted text messages.

Main components:

Component Role Technology
User interface Retro terminal UI Plain HTML/CSS
Signaling Makes the two peers "find" each other PeerJS (public cloud broker)
Data transport Encrypted P2P channel WebRTC DataChannel
NAT traversal Punching through firewalls/NAT STUN + TURN (ICE)
Encryption Message content protection AES-GCM 256-bit + PBKDF2
Hosting Code distribution GitHub Pages (static)

There is no proprietary application server: the code runs entirely in each user's browser. The only external infrastructure involved is used to "introduce" the two devices to each other (signaling) and, if needed, to relay traffic when a direct connection isn't possible (TURN).


🔄 Operational Flow

Room Key Generation

When a user clicks "INITIALIZE ROOM (HOST)" :

  1. A random 100-character string is generated ( generate100CharCode() ), using crypto.getRandomValues() — a cryptographically secure random number generator (not Math.random() , which is unsuitable for cryptographic purposes).
  2. The character set includes uppercase/lowercase letters, digits, and special symbols ( -_!@#$%^&* ), maximizing entropy within 100 characters.

This string (the Room Key ) is the only shared secret the two parties need to exchange, out-of-band (e.g. voice message, in person, another encrypted channel).

Deriving Keys from the Room Key

Two independent values, each with a different purpose, are derived from the Room Key:

A. Message encryption key (PBKDF2 → AES-GCM)

PBKDF2(
  password = Room Key,
  salt = "p2p-zero-trace-salt-v1" (fixed, hardcoded),
  iterations = 100,000,
  hash = SHA-256
) → 256-bit AES-GCM key

B. PeerJS identifier (truncated SHA-256)

SHA-256(Room Key) → first 32 hex characters, prefixed with "ztt-"

This ID is used solely so that Host and Guest can "find" each other on the PeerJS signaling broker, without exchanging anything beyond the Room Key. It plays no cryptographic role.

Note on the fixed salt: the PBKDF2 salt is hardcoded and identical across all sessions. This is acceptable because the "password" (Room Key) already has very high entropy (100 random characters) — a fixed salt only weakens security in scenarios involving weak, reused passwords, which does not apply here.

Signaling Phase (PeerJS)

  1. The Host creates a Peer object, registering with the public PeerJS cloud broker using the ID derived from the Room Key.
  2. The Guest, after pasting the same Room Key, computes the same ID and calls peer.connect(id) .
  3. The PeerJS broker only mediates this initial exchange (who wants to talk to whom) — it never sees or transmits message content , which by that point travels over a separate WebRTC channel.

ICE Negotiation (NAT Traversal)

Once the two Peers have "introduced" themselves, WebRTC starts ICE negotiation to find a valid network path:

  1. Host candidates — the device's local IP addresses
  2. Server-reflexive (srflx) candidates — public IP discovered via STUN
  3. Relay candidates — allocated via TURN, used only if a direct connection fails

Configured ICE servers (in priority order):

  • Dedicated Metered.ca TURN (own credentials, not shared) — stun.relay.metered.ca / global.relay.metered.ca
  • 7 public STUN fallbacks (Google ×3, Cloudflare, Twilio, Nextcloud, stunprotocol.org, freestun)
  • 10 additional public TURN fallback endpoints (OpenRelay, freestun, numb.viagenie, ExpressTurn) — used only if the dedicated TURN also fails

The browser automatically tries every combination and selects the first one that establishes a working channel (standard ICE algorithm, handled internally by WebRTC).

Timeout and Session Expiry

  • If the connection isn't established within 120 seconds , the session is considered expired:
    • The Peer and DataConnection are destroyed ( peer.destroy() , conn.close() )
    • The status shows [EXPIRED] Room Key no longer valid
    • A button appears to generate a new Room Key (Host) or enter a new one (Guest)
  • This prevents a Room Key from remaining "listening" indefinitely on the public broker.

🔐 Message Cryptographic Model

Every message is individually encrypted before being sent over the DataChannel:

1. Generate a random 12-byte IV (crypto.getRandomValues)
2. ciphertext = AES-GCM-Encrypt(key, IV, plaintext)
3. payload = IV || ciphertext   (concatenated, IV in plaintext at the front)
4. Send payload as a Uint8Array via conn.send()

On receipt:

1. Extract the first 12 bytes as the IV
2. The rest is the ciphertext (includes the 16-byte GCM authentication tag at the end)
3. plaintext = AES-GCM-Decrypt(key, IV, ciphertext)

Security properties guaranteed by AES-GCM:

  • Confidentiality — nobody without the key can read the content
  • Integrity/authenticity — any tampering with the packet in transit causes decryption to fail ( [ERR: DECRYPTION_FAILED] ), rather than silently producing corrupted output

What this scheme does NOT cover:

  • Forward secrecy across sessions — if the same Room Key were reused across multiple sessions (not the normal flow, which generates a new one every time), all those sessions would share the same derived key
  • Peer identity authentication — anyone who knows the Room Key can connect; there is no cryptographic verification of "who" is on the other end beyond possession of the shared key

💾 Data Persistence (Client-Side)

Data Persistence Notes
Room Key None Only in a JS variable, gone on close/reload
Derived AES-GCM key None Same, never written to disk
Chat messages None Only live in the DOM/RAM, no localStorage/IndexedDB
Cookies None The project uses none at all
Application logs Local DevTools console only Never sent anywhere, gone when the tab closes

The "PANIC: PURGE SESSION" button explicitly forces:

  • Closure of the PeerConnection / DataConnection
  • Zeroing of the encryption key in memory
  • Wiping of all UI fields and the displayed message history

👁️ What External Infrastructure Can See (Metadata)

Key point to understand: encryption protects content, not connection metadata .

Service What it can see What it CANNOT see
GitHub Pages IP and timestamp of whoever loads the page Message content, Room Key
PeerJS broker (public cloud) IP of Host and Guest, when they connect, their Peer ID (a hash of the Room Key, not the Key itself) Message content
Metered.ca TURN (if used as relay) Source/destination IP, ports, amount of data transferred Message content (already travels encrypted)
Each user's ISP That a connection is being made to github.io / metered.ca / a PeerJS server Message content

Recommended mitigation (outside the code): use a trustworthy VPN (e.g. Mullvad, with an anonymously created account) on both devices, to avoid exposing real IP addresses to these third-party services. The project displays an explicit warning to this effect on the splash screen.


🎯 Attack Surface and Known Limitations

Risk Description Mitigated?
Message content interception MITM on TURN/network traffic ✅ Yes — end-to-end AES-GCM
Message tampering in transit Packet manipulation ✅ Yes — GCM authentication tag
Deanonymization via IP metadata IP↔identity correlation through third-party logs ⚠️ Partial — requires a VPN client-side, not solved by the code itself
Metered.ca account compromise TURN credentials are in the public code (base64-obfuscated, not encrypted) ⚠️ Minimal deterrent, not real security
Dependency on third-party services GitHub Pages, PeerJS broker, Metered TURN — if suspended, the app stops working ⚠️ Not mitigated (would require full self-hosting)
Room Key reuse Would compromise forward secrecy across sessions that reuse it ✅ Not applicable in normal flow (a new Key every session)

🛠️ Full Technology Stack

  • Frontend: HTML5, CSS3 (no framework)
  • Cryptography: Browser-native Web Crypto API ( crypto.subtle ) — PBKDF2, AES-GCM, SHA-256
  • P2P/Signaling: PeerJS v1.5.4 (a wrapper library over native WebRTC), loaded from a public CDN (unpkg.com)
  • NAT Traversal: WebRTC ICE (STUN/TURN) — 18 endpoints configured in total
  • Hosting: GitHub Pages (static, automatic HTTPS)
  • Browser requirements: WebRTC support, Web Crypto API, ES6+ — requires a secure context (HTTPS); does not work from file:// or content://

📝 Changelog of Major Versions

  1. v1 — Native WebRTC with manual SDP exchange (copy/paste offer/answer)
  2. v2 — Migrated to PeerJS, automatic connection based on the Room Key, removed manual SDP fields
  3. v3 — Added dedicated Metered.ca TURN + multiple public STUN/TURN fallbacks
  4. v4 — Detailed ICE diagnostics (candidate logging, connection states) — fixed a bug that overwrote PeerJS's internal event handlers
  5. v5 — Timeout extended to 120s, Room Key expiry system with manual regeneration, VPN warning on splash screen, TURN credential obfuscation

💚 Support the Project

Hard-Chat is 100% free, open-source, and maintained by the Hardlint Cybersecurity Team. We don't run ads and we don't sell data. If you believe in our mission and want to help us fund our future self-hosted infrastructure (custom STUN/TURN servers), consider supporting us!

Solana (SOL) donation address:

GSsqZCtDC7rf53U6gC5cJ4weAYYT9g7twxz9t15mfRDV

📜 License and Disclaimer

This software is provided "as is", without warranties of any kind. The developers are not responsible for any improper or illegal use of this tool. Users are solely responsible for complying with applicable laws in their jurisdiction.

This document is provided for informational and technical documentation purposes only. It does not constitute legal advice regarding regulatory compliance, privacy, or liability for use.


Hardlint Cybersecurity Team

Research acceleration: The view inside OpenAI

Simon Willison
simonwillison.net
2026-09-06 19:57:40
Research acceleration: The view inside OpenAI Apparently today is RSI day at OpenAI, for Recursive Self-Improvement - I think it's their new AGI. Both this piece and the new essay An Alien Mind (by Chief Scientist Jakub Pachocki) talk about it, and this one doesn't even bother to expand the acronym....
Original Article

6th September 2026 - Link Blog

Research acceleration: The view inside OpenAI . Apparently today is RSI day at OpenAI, for Recursive Self-Improvement - I think it's their new AGI. Both this piece and the new essay An Alien Mind (by Chief Scientist Jakub Pachocki) talk about it, and this one doesn't even bother to expand the acronym.

Included are details on how OpenAI's own research team are using coding agents. Like pretty much everyone else 2026 has been the year that agentic engineering really took off at OpenAI, best illustrated by this chart:

Screenshot of a line chart from a report, headed "1. Coding agents are reshaping daily work for OpenAI researchers" with a partially visible chart title ending "significantly—Median researcher". Y-axis: "Daily $ / researcher" from 0 to 700. X-axis labels: "Feb 2026", "Apr 2026", "Jun 2026", "Aug 2026". A blue line stays near 0 through February, rises slowly to about 50 by April and 150 by June, plateaus around 150–165 into July, then climbs steeply to roughly 600 by late August 2026.

I'm intrigued at what caused that significant acceleration in AI spend per researcher in late July - my best guess is that's when internal employees gained access to the model later released as GPT-6 Astra.

Four Weeks of a Vegan Diet Alter Signs of Inflammation and Aging

Hacker News
www.uniklinik-freiburg.de
2026-09-06 19:44:35
Comments...
Original Article

Freiburg, 08/27/2026

Nutrition study involving 48 healthy adults / Researchers analyze more than 800,000 genetic loci / Evidence of changes in inflammatory processes, metabolism, and biological aging


Just four weeks of dietary changes can already be reflected in epigenetic changes. This is shown by a study conducted by the University Center for Naturopathy at the Medical Center – University of Freiburg and the University of California, San Diego, involving 48 healthy adults. The research team led by PD Dr. Maximilian Storz, Lukas Karbacher , M.Sc., and Prof. Dr . Jerome Mertens (University of California, San Diego) investigated how a vegan diet or a diet particularly high in meat affects epigenetic marks on the surface of DNA. In the process, they found evidence of changes in biological signaling pathways associated with, among other things, inflammatory responses, cell growth, and biological aging. The results were published on August 3, 2026, in the journal MedComm .

“Our findings show that a dietary change can trigger measurable changes at the epigenetic level within just a few weeks,” says Storz, a specialist at the University Center for Naturopathy at the Department of Medicine II at the Medical Center – University of Freiburg and one of the study’s lead authors. “Now, larger and longer-term studies must show whether these changes are permanent and how they can be integrated into existing treatment regimens.”

More Than 800,000 Genetic Locations Analyzed

After a one-week period of standardized nutrition, the participants were randomly assigned to two groups: 24 followed a vegan diet for four weeks, while 24 followed a diet particularly high in meat. Energy intake was to remain as constant as possible. Blood samples were collected before and after the dietary change. In total, the research team analyzed approximately 800,000 DNA sites for so-called methylation—chemical marks that can influence how active certain genes are.

After four weeks, changes were observed in several biological signaling pathways in the vegan group. Among other things, these changes affected processes that regulate cell growth, metabolism, and cell repair. For example, the activity of the mTOR signaling pathway was reduced; excessive activation of this pathway can promote uncontrolled cell growth. At the same time, signaling pathways associated with insulin action, cell repair, stress responses, and healthy aging were more strongly activated.

The researchers also found differences in the immune system: In the vegan group, there were fewer neutrophils—a type of white blood cell that can promote inflammatory responses—and more CD4+ T cells, which play an important role in regulating the immune response. Overall, this pattern suggests a less pro-inflammatory immune response.

Original title of the study: A vegan diet epigenetically modulates inflammatory pathways and biological aging: Genome-wide DNA methylation analysis of a one-month isocaloric vegan versus meat-rich dietary intervention
DOI: 10.1002/mco2.70899
Link to the study: https://doi.org/10.1002/mco2.70899


Back

Making a Python interpreter in 1024 bytes

Hacker News
austinhenley.com
2026-09-06 19:14:08
Comments...
Original Article

Austin Z. Henley

I build tools for people



9/6/2026

A screenshot of the 1024 bytes of golfed C code.

To feel human, I write code by hand on the weekends.

My latest challenge? Make a Python interpreter in 512 1024 bytes of good ole C code. Oh, and no macro shenanigans or library tomfoolery.

def buzz():
    for n in range(101):
        if n % 15 == 0:
            print("FizzBuzz")
        else:
            if n % 3 == 0:
                print("Fizz")
            else:
                if n % 5 == 0:
                    print("Buzz")
                else:
                    print(n)
buzz()

I probably can't fit all of the Python language into an interpreter that is only 1024 bytes of code. So what can I fit that will look like Python?

This fizzbuzz program looks distinctly Python. It has the def , the colons, the indentations, and no parentheses for if statements. Looks like Python to me! Of course, I'll also have to add some additional limitations beyond just a subset of the syntax.

My first attempt was bad though.

First try: 512 bytes is not enough!

I've written many recursive descent parsers, so how different can this be? A subset of Python should be similar to the other languages I've implemented.

I started with the most basic code I could think of: 1 + 2

Then I made it more complex: x = 1 + 2 * 3

And then I even added statements: if x > y: z = 3

Great, I made a calculator... Not what I meant with this challenge! I was already over the limit too. That is when I zoomed out and made a list of elements that look Pythony, while also realizing that my code golf skills were not up to snuff to make it fit in 512 bytes.

Maybe I can do it in 1024 bytes? First, make it work, and then make it small.

The parser

The actual CPython implementation tokenizes the Python source, parses it into an abstract syntax tree, performs some analysis and optimizations, emits bytecode, and then interprets the bytecode.

This won't really do any of that.

The state is held in a handful of global variables. It uses a fixed-length array (999 for now) that will hold the raw Python code. The variables and function names all fit into a single array.

char src[999];       /* Entire program without most spaces. */
int  vars[256];      /* Symbol table.                       */
int  pos;            /* Next character in src.              */
int  ch;             /* Current character in src.           */
int  line_start;     /* Where the current line starts.      */

The expressions are handled like any other recursive descent parser, and they are executed along the way. For example:

int parse_sum(void) {
    int value = parse_term();
    while (ch == '+' || ch == '-') {
        if (ch == '+')
            value = value + parse_term();
        else
            value = value - parse_term();
    }
    return value;
}

Straightforward so far.

There is no error handling of any kind! It makes a lot of assumptions based on the correctness of the code. For example, it assumes that the keywords are all typed out correctly.

    if (ch == 'w' || ch == 'i' || ch == 'f') {
        /* ---- while / if / for ---- */
        int keyword = ch;
        int loop_var = 0;

        if (keyword == 'f') {             /* "for K in range(N):" */
            pos += 2;                     /* skip "or"             */
            loop_var = next();            /* the loop variable     */
            pos += 8;                     /* skip "inrange("       */
            vars[loop_var] = 0;
        } else if (keyword == 'w')
            pos += 4;                     /* skip "hile"           */
        else 
            pos += 1;                     /* skip "f" of "if"      */

It also assumes the token boundaries are correct and strips out most whitespace. It keeps indentation and spaces in string literals.

It is limited to variable names of a single, lowercase character, which allows us to do symbol table lookups directly:

    if (ch > 96) {
        value = vars[ch];
        next();
    }

Control flow magic

The function for executing blocks of code continues until the indentation decreases. When that happens, it returns, and it is up to the caller to handle the next line. So, it is using the C program's call stack to handle the recursion.

void run_block(int min_indent) {
    for (;;) {
        int indent = read_indent();

        if (ch == '\n')                       
            continue;

        if (indent < min_indent || ch == 0) {
            pos = line_start;
            return;
        }

But what about loops?!

Since nothing is compiled, loops work by jumping backwards and reparsing the source each iteration. Both while and for loops keep track of the position of the condition expression. After the body executes, it jumps back to that position and continues parsing.

Functions work in the same way. When parsing the definition, the symbol table remembers the position of the function in the source code. Then when parsing a function call, the caller location is saved, the parser jumps to the function body, executes the body, and restores the caller location when it reaches the end.

It is quite beautiful what we can do even with no intermediate representation! The interpreter maintains very little state too.

Minify!

I haven't code golfed much. Trimming the variable names and whitespace is obvious, but how do I save the big bytes?

There exists an ancient, forgotten website called Stack Overflow where the code magicians of yesteryear shared their knowledge. I learned a lot of ideas from Tips for golfing in C .

Since rules only exist in your imagination, I did have to get creative. Some of those tips rely on "features" specific to GNU C89. This is not tomfoolery! This is conventional fiddle-faddle. Here is what I did to shave off bytes from the readable version:

  • Single-letter variable and function names
  • Assume the compiler will link libc
  • Use globals for temp variables
  • Globals are zero initialized
  • C89 allows variable declarations to be implicitly int and functions are assumed to return int
  • Use function parameters as temp variables that are preserved on the call stack
  • ASCII values instead of character literals
  • Ternary operator and comma operator
  • Bitwise operations instead of logical operations

For example, the parse_sum(void) function that I showed earlier was golfed down to e(){for(z=t();c-43u<3;)y=44-c,z+=y*t();return z;} . It uses ASCII values to shave a few bytes.

After everything, the golfed version is 1024 bytes!

The final readable version is over 4800 bytes. I originally had several more features but I kept cutting to make it fit. The comparison expressions were next on the chopping block, since that eats up a lot of bytes and truthiness still works without them: if n%15: .

If all I cared about was making fizzbuzz work, I think I could get below 800 bytes! There are probably other golfing tricks too.

A screenshot of a terminal checking the byte length of the golfed code, compiling it, and running fizzbuzz with it.

Here is the golfed source in all its glory:

char s[999];v[256],p,c,x,y,z,w,u;G(){return c=s[p++];}I(){for(u=p;G()==32;);return p-u;}Y(){c&&c-10&&Y(G());}f(){x=0;if(G()>96)x=v[c],G();for(;c-48u<10;G())x=x*10+c-48;return x;}t(g,h){for(g=f();c==42|c==37;)h=c,g=h-42?g%f():g*f();return g;}e(){for(z=t();c-43u<3;)y=44-c,z+=y*t();return z;}E(a,q){a=e();if(c-60u>2)return a;w=c-61;q=G()==61;p-=!q;x=e();return w?(a-x)*w>-q:a==x;}S(i){for(;I()>i|c==10;)Y();p=u;}Q(){for(G();G()-34;)putchar(c);G();}B(i,q,j,k,a,m,n){for(;;){j=I();if(c==10)continue;if(j<i|!c){p=u;return;}if(c==119|c==105|c==102){k=c;k-102?p+=k/4-25:(p+=2,m=G(),p+=8,v[m]=0);q=p;for(;;){a=k-102?E():v[m]<E();p+=k==102;G();if(!a){S(j);break;}B(j+1);if(k==105)break;k-102||v[m]++;p=q;}I()-j|c-101?p=u:(p+=4,G(),a?S(j):B(j+1));}else if(c==100){p+=2;k=G();Y();v[k]=p;S(j);}else{if(c>96){k=c;while(G()>96);c==40?k-112?(G(),n=p,p=v[k],B(2),p=n,G()):(s[p]-34?printf("%d",E()):Q(),puts(""),G()):(v[k]=E());}Y();}}}main(q,m,h){for(h=m=q=0;~(c=getchar());){c=c-9?c:32;h^=c==34;s[q]=c;q+=c-32?1:!m|h;m=c>32|m&&c-10;}B(0);}

In the end, I was able to implement these features:

  • Integer variables (single letter) and literals
  • Variable assignment
  • Arithmetic with + - * % with precedence (unary + - only works at the beginning of an expression)
  • Comparisons with < > <= >= == (only one per expression)
  • Integer truthiness
  • if and else
  • while loops, including else blocks
  • for x in range(y) loops, including else blocks
  • Function definitions with no arguments
  • Function calls, even recursive
  • Indent-based blocks (without scope)
  • print with a single string literal or integer expression
  • Comments

I don't think I will be doing any code golf challenges again in the near future. The process was quite tedious, going back and forth between the gulfing-in-progress version and the original version to try to understand what I changed just 2 minutes ago. Both versions are on GitHub .

Now it is your turn. What does your Python in 1024 bytes look like?

Is mathematics about to enter the conservatory?

Hacker News
mbmccoy.dev
2026-09-06 19:02:11
Comments...
Original Article

The same week that Claude finished formalizing the proof of Fermat’s Last Theorem in Lean , a paper landed in my inbox titled, The Spherical Hadwiger Theorem . The Spherical Hadwiger Conjecture 1 , which has been open since about 1974, describes a niche-but-important piece of integral-geometric machinery. I’m not going to get into the details of the conjecture here; if you are interested you can see a discussion in my previous post where the theorem (then still a conjecture 2 ) greatly simplifies the proof of a little lemma of mine from grad school.

But to the point: this new preprint by Wang & Wu of Hunan University apparently proves the conjecture using AI assistance. The final section contains the disclaimer:

During the preparation of this manuscript, OpenAI Codex was used to assist with developing proof details, identifying gaps and points requiring clarification, organizing and typesetting the manuscript, and editing the English. The authors reviewed and verified all AI-assisted mathematical content and suggested changes, made all final mathematical and editorial decisions, and take full responsibility for the manuscript.

This disclaimer leaves open the possibility that Codex did a substantial portion of the work that, until very recently, required a research-level mathematician: developing proof details 3 , finding and fixing gaps, and apparently writing the paper. Moreover, the work is very polished and readable (if you are a research mathematician in this field).

To be clear, I haven’t fully verified the proof; I worked through it with Claude Fable and it passes the sniff test, but fully digesting it will take a bit more energy than I have right now. None of this is a knock on Wang & Wu—this seems to be a great paper, and is worth digesting. They’ve even followed all the principles for AI use laid out in the Leiden Declaration .

A milestone, close to home

For me, the proof of the Spherical Hadwiger Theorem hits home. I tried to prove it in grad school, and made a half-hearted attempt again with AI assistance earlier this year. It’s not a headline-grabbing theorem. That didn’t save it.

I shouldn’t have been surprised. When GPT-4 launched, OpenAI released a report on the potential labor impact of LLMs. The exposure of the work of mathematicians to disruptions from AI was the highest of any category they modeled; the whitepaper estimated that 100% of a mathematician’s job was exposed to LLMs, across three distinct labor models. Higher than writers, translators, artists, and graphic designers. The only difference is that it took a bit longer for mathematicians to begin to feel the pain.

It’s tempting, if somewhat arrogant, to claim that this delay in LLM dominance in mathematics arose because research-level mathematics is among the most challenging human endeavors. I suspect the delay owes as much to research mathematics having less economic value—and less training data—than these other creative domains.

Consider classical music. Our society does not support classical musicians in the same way that we support ‘popular’ musicians. Classical music has been institutionalized, sent to the conservatory as a relic. A small segment of society has decided the ability to perform it is worth preserving, and devotes a sliver of capital to that end: training young people, and paying a few of the best players in the biggest cities to do it professionally.

Could this model work for mathematicians? It’s easy to imagine: in Euclid’s time, mathematics largely existed as an intellectual pursuit worthy of a few inclined people. A mathematical conservatory could help math flourish even when it is no longer hard to create new results, just hard to understand and communicate their import.

But pure mathematics is already in a conservatory, better known as the academy . Outside of the university, the jobs for pure mathematicians remain slim. The results are only understood by a select few. Does it even matter that the hard results are all going to be proved by computers soon?

The bottom line

It’s worth supporting people to continue the cultural endeavor that we currently call “research mathematics.” Mathematics, especially pure mathematics, has always been about communicating stories that help us understand reality more deeply. By simplifying and abstracting, we begin to see the hidden structure: parallel lines never cross, the sphere looks the same in every direction, the primes never end. But the current support systems for mathematicians, like so many other human creative fields, need to change drastically to handle the new realities of AI.

But the incentive structures that support this work, like those in so many other creative fields, are ill-suited to what AI is bringing. These tools make it harder to tell whether a complex argument is even correct, much less who deserves funding, tenure, and fame. The choice before us is how do we continue to make humans matter when their intellectual labors simply don’t compare to computers. When intelligence is limited only by silicon and electricity, will we be willing to continue to support a culture that has mathematicians? I sure hope so.

Get new posts by email. No spam, just posts.

Hackers have withdrawn ~4k BTC (~$320M) from the Liquid Federation wallet

Hacker News
twitter.com
2026-09-06 18:43:25
Comments...
Original Article

We are aware of a security incident on

@ Liquid_BTC

. Purported white-hat hackers have withdrawn ~4,000 BTC (~$320 million) from the Liquid Federation wallet. The

@ Blockstream

team is working on contacting them on-chain with a signed message. What we know so far is that the funds were withdrawn via the SideSwap PAK (Peg-out Authorization Key), but that key was not compromised, nor were any others. Exchanges have been notified and have already paused (or will pause) LBTC deposits and withdrawals. Other Liquid assets such as USDT, DePix, and RWAs are unaffected by this security incident. Bridge nodes have been temporarily disabled, so no new transactions can be submitted to the network. Effectively, the Liquid sidechain is paused until this issue is resolved. Liquid wallets will be impacted, and we're sorry for any inconvenience. Federation members are actively working on resolving this so we can restore normal network activity. You can monitor the situation via

@ mempool

's liquid.network site below: mempool.space/address/bc1qdl…

Untangling Lifetimes: The Arena Allocator (2022)

Lobsters
www.dgtlgrove.com
2026-09-06 18:38:08
Comments...
Original Article

When I’ve said that I prefer to write my software in C, a common response is raised eyebrows. Dominant memes in software culture make my position unpopular. “Why would you write new code in an unsafe systems language?”, “performance isn’t everything!”, and perhaps the most common, “why subject yourself to the requirement of manually managing memory?” .

There exists a prevalent perception that manual memory management is difficult to do, difficult to do correctly , and inherently bug-prone and unstable. This way of thinking was repeatedly peddled throughout my university computer science experience. Managing memory manually in C was a temporary endeavor, only to peek under the hood—strictly for academic purposes. The idea that anyone would ever actually do manual memory management in C these days was just unthinkable—I mean, after all, it’s current year !

As I experienced it, education around memory management was for historical understanding. How did the Linux kernel originally do memory management? Let’s do an assignment, so you can see how gross it is! Oh no, a “raw” malloc ! But don’t worry. Next class, you can return to the padded room, where memory-related bugs and instabilities are “impossible” (or so they claim).

After exposure to the dark underworld of manual memory management, your professor rescues you, and introduces automatic reference counting, RAII, and/or garbage collectors as the “solutions”.

I find this nonsensical. It’s not that “normal” methods of memory management in C are fine—there is an enormous amount of poorly written C code.

My stance, on the other hand, is that programmers are taught to overcomplicate the problem . After programming for several years, I’ve seen that almost all memory management problems are simpler than they first appear . By taking advantage of a problem’s concrete constraints, memory management can be easy, performant, and flexible.

In this post, I’ll present an alternative to traditional strategies of manual memory management that I’ve had success with. Let’s begin by analyzing “manual memory management in C” as it’s normally presented—the classic malloc and free interface—and its consequences.

malloc —which is short for “ m emory alloc ate”—is an API to which you pass some number of bytes that you need for a dynamic allocation, and returns to you a pointer to a block of memory that supports that many validly-accessible bytes.

The symmetric counterpart to malloc , free , just expects a pointer that you got from malloc , and it guarantees that whatever block of memory that pointer points to will be made available for subsequent calls to malloc .

The malloc and free interface was built to support usage code that wants to dynamically allocate blocks of memory of arbitrarily-different sizes , with each allocation having an arbitrarily-different lifetime . There is no restriction on either of those two factors, meaning the following usages are all valid:

The most common (and understandable) criticism of malloc and free , or what they call “manual memory management in C”, is that using it for granular allocations with varying lifetimes across several layers in a codebase can easily lead to a rat’s nest of complexity. In these rat’s nests, it’s easy to accidentally free the same pointer twice, to access memory in a block that has already been free ’d, to forget to free a pointer altogether (causing a leak), or to force your program to suffer computationally because of a need to free each small allocation in, for instance, a complex data structure with many nontrivial links between nodes.

The worst of these mistakes can lead to serious security and reliability issues. Imagine that memory is allocated with malloc , then freed once , then mistakenly freed again. An allocation may occur between the first and second call to free , which possibly reuses the already-released (at that point in time) memory. Because the usage rules of malloc and free have been broken, the allocator’s implementation and user disagree on an important detail—whether or not the allocation reusing portions of the first is allocated or not.

As I’ll present in this post, these rat’s nests can be avoided. But why, then, are they seemingly so common in C codebases, and why is there a dominant perception that they are unavoidable without more complex compiler and language features?

malloc and free enforce very little on their usage code, so there is a large space of possibilities in how malloc and free are used (as the number of constraints increases, the number of solutions decreases). Many of those possibilities are the ever-common rat’s nests. The natural path for many C codebases is simply that of least (initial) resistance, which is to assume malloc and free as a suitable memory allocation interface (which is not necessarily unreasonable, given a lack of data), and so they will adopt it as a pattern. In the case of malloc and free , that means adopting a large space of possibilities—including the subset of those possibilities which include misuse, or explosions of complexity (“rat’s nests”).

There is a philosophy of abstraction in the programming world that believes in providing a certain desirable (for one reason or another) interface irrespective of the implications that interface has on its implementation. It is this philosophy that also claims that an interface can remain stable even with the implementation of the interface wildly changing. For some reason, it took several years before I became aware that the reality is precisely the opposite—an interface and its implementation are intrinsically related in subtle ways.

There is, of course, some degree to which an interface may remain stable with modifications to its implementation, but when the nature of the implementation must change, the interface must also fundamentally change, at least to avoid introducing unnecessary distortions to a problem (for instance, an interface being simply malformed for a given implementation, result in performance issues, bugs, underpowered APIs, and so on). The interface certainly hides some details , but it also explicitly does not hide others—in fact, an interface itself is defined by exposed guarantees or constraints, which both the user of the interface and the implementation must agree upon. There is certainly a more precise way to formally demonstrate this, but for now, I’ll leave it at that.

malloc and free serve as a useful example of how an interface’s definition is closely related with both its usage patterns and implementation. The fine-grained control over an individual allocation’s size and lifetime—to allow for arbitrarily overlapping lifetimes with arbitrarily different sizes—result in very few constraints on the usage code. This causes, firstly, complications in the implementation of the allocator. While that causes performance problems (and thus has caused a popular meme that “dynamic allocation in a hot loop is bad”), the worst of the issues arise in the patterns relating to usage of a malloc and free style interface.

Iterating all of these patterns would be impossible, but I’ve gathered a few for this post to help illustrate the issue.

You’ll notice that—in all conversations about manual memory management in C—the common case of memory allocation is never discussed, because it mostly stays in the background, is trivial to use, and more-or-less works correctly and invisibly: the stack.

Sometimes, however, the stack is not an option—I’ll get more into that later. But when malloc and free (or equivalent) have been adopted as the default memory allocation pattern in a codebase, they are the first choice whenever the stack stops being an option. This is a common pattern in several C codebases in the wild, which—knowingly or not—have been corrupted by object-oriented thinking, even if their writers do not explicitly think that is the style of thinking they are using. When an individual allocation doesn’t work on the stack, for whatever reason, the author of the codebase in question is often taught that the only other option is to use “heap allocation”, which to most people means “use malloc ”. So, instead of using the stack allocator , they will switch to the extremely generic heap allocator, often being unaware that these are not the only two choices.

As such, these codebases will have de facto objects, and these objects will generally have some initialization mechanism, and some deinitialization mechanism. If the codebase adopts a rule that suggests these objects may be allocated and deallocated in the same way an individual malloc allocation may be allocated and deallocated (which is the natural path of least resistance, which requires the fewest new possibly-bad assumptions), then the object’s interface will wrap the malloc and free interface, and follow the same rules pertaining to lifetimes:

When the above style of interface becomes a rule within a codebase, it is frequently built by default without accounting for a number of actual constraints which may have otherwise simplified the problem:

  • When does usage code actually need a MyObject ?

  • How many of them does it need?

  • Is a MyObject only required given the presence of another object?

  • How easily can you predict how many are needed?

  • If multiple, are they freed all at once, or one at a time? In what order?

  • How important is it that a MyObject is released at all?

  • Is it important to be able to keep track of all MyObject allocations independently from other allocations?

When such questions are ignored, the above pattern of wrapping a totally-generic allocation/deallocation interface becomes common parlance in a codebase, which leads to a proliferation of code that must assume full responsibility for finely-managing the lifetime of any individual object that doesn’t fit stack allocation (either because it has dynamic runtime requirements, alloca is not an option, it’s too large, or it doesn’t have lifetime requirements that fit the stack). This leads to this pattern being used even for very granular “objects” , which explodes the number of actual dynamic allocations and deallocations, and makes it far more likely that a programming mistake occurs. Pairing a single malloc with a single free is easy—pairing 1,000 malloc s with 1,000 free s—especially when many of those individual malloc s have dependencies on others—is dramatically more difficult to write once , and especially more difficult to maintain overtime .

If the problem of managing 1,000 (or 10,000, or 100,000) various lifetimes wasn’t enough of a problem for you, now consider that these lifetimes often have complex dependencies on one another. Eventually, there is a graph of lifetimes, each node (lifetime) in which relies on certain assumptions about some number of other lifetimes. “Object A”, within its own lifetime, will refer to “Object B”—care, then, must be taken to ensure that, for instance, “Object B” is not freed before it is accessed through “Object A”.

If there is no organizing principle around managing these relationships and their corresponding lifetimes, through a number of subtle mistakes (that often do not arise immediately) a codebase quickly turns into a sludge, where important work is constantly deferred behind bugfixing or maintenance work—or, worse, where important work occurs before bugfixing and maintenance work, and thus bugs and maintenance issues accumulate over time.

What a memory leak literally is on a modern computer is very often glossed over in programming education. It is very frequently perceived as a scary no-no—if your program has a leak, it’s a bad program, and you’re a bad programmer, and you will go to programmer hell!

With this perception, programmers will often carefully free allocations in their program to the point of religiosity , even when doing so is strictly worse than never writing a single line of cleanup code.

To clear this up, I’ll first explain—literally—what happens when you allocate something with malloc on a computer these days, and then subsequently what happens when you fail to call free with a pointer returned to you from malloc .

When you first call malloc , you’re ultimately just calling a function that was implemented by whoever wrote the implementation of the C runtime library that you’re using. The person who wrote that code had a task—implement a dynamic memory allocator, given the constraints in the C specification. So, their job is to return you a pointer to a block of memory that’s at least as large as what you asked for, and then to also be able to release that memory (make it available for re-allocation in a later malloc ) in the free implementation.

On a modern machine like the computer or phone you’re reading this on, at some point the allocator will ask the operating system for memory dynamically. Part of the utility of the C runtime library is, at the end of the day, simply abstracting over operating-system-specific code. To do this, it will request that the operating system maps new pages into the relevant virtual address space (by calling an operating system API, like VirtualAlloc on Windows). The pointer that malloc returns to you is not literally an address of any physical memory—it’s instead an address in your own virtual address space . The operating system, then, manages a mapping data structure, called a “page table”, which maps virtual addresses into physical addresses.

Being called by VirtualAlloc (or similar), the operating system will make adjustments to that mapping data structure, which changes the mapping between sections of a virtual address space and physical pages in memory. After that is complete, the operating system—whenever it chooses to schedule your program’s thread—can then prepare the memory management unit (MMU) accordingly, so that whenever your thread asks about an address, it can map to the appropriate physical address.

The main point being, whenever a process crashes (hits a hardware-level exception, like a page fault which it cannot resolve) or normally exits, the operating system continues running (or, at least, it had better), and has a whole understanding of what pages in your program’s virtual address space were mapped to physical memory addresses. So the operating system is able—and really, required, in the presence of code that cannot be trusted to never crash—to “release” the physical pages that your process originally asked for.

So, tying that all together, what happens when you never call free with a pointer that was returned to you by malloc ? The first obvious point is that—of course—the malloc / free allocator fails to ever see the pointer again, and so it assumes it is still “allocated” by the usage code, which means that memory can never be reused again for another malloc allocation. That is what is called a “memory leak”. On the program exiting or crashing, no resources are truly “leaked”.

Now, to clarify, a leak may actually be a problem. For instance, if you’re building a program that runs “forever” to interact with the user through a graphical user interface, and on every frame, you call malloc several times and never free the memory you allocate, your program will leak some number of bytes per frame . Considering that—at least in dynamic scenarios—your program will be chugging through a frame around 60, 120, 144 times per second, that leak will likely add up fairly quickly. It’s possible that such a leak would prohibit normal usage of the program—for instance, after 30 minutes, an allocation failure occurs, or the operating system spends far too much time paging in memory from disk to allow your program’s memory usage to continuously grow.

On the other hand, however, a leak is very often not a problem —for example, when allocating memory that needs to be allocated for the duration of the program, or if you expect the program to only boot up and perform a task, then to close.

In other words, you may treat the operating system as “the ultimate garbage collector”— free ing memory when it is unnecessary will simply waste both your and the user’s time, and lead to code complexity and bugs that would otherwise not exist. Unfortunately, many popular programming education resources teach that cleanup code is always necessary . This is false.

The malloc and free interface is symmetric—for each call to malloc that returns a pointer to memory that must be freed, there will be one call to free . So for instance, if memory for an entity in a game world is malloc ’d when the entity is spawned, then it will be free ’d when the entity is killed or deleted.

A problem arises when the manner in which memory is acquired does not match the manner in which it is ultimately released. For example, if I load a level in my game engine’s level editor, and for each entity that is spawned some number of malloc s occur, and for each entity I spawn while editing the level some more malloc s occur, then I want to unload that level and load a different one, then there is no “free all of the memory I allocated for this level” button. Each malloc must receive its symmetric free , and so I must iterate all of the entities and free each result that was returned to me from malloc .

There is no “ malloc checker” in C compilers, and there is no obvious element of incorrectness that comes from forgetting to free something in such a codepath (unlike the element of incorrectness you’d quickly find on forgetting to allocate something), and so it’s quite easy to allow the “free everything” path to become out-of-sync with the “allocate one thing” path. This leads to both bugs and leaks—and, of course, it contributes to the sum-total of code required to implement something.

The above patterns are surely not exhaustive, but they hopefully provide a decent picture of how the nature of an interface like malloc and free can spiral into an out-of-control mess, where bugs and leaks regularly arise. This mess will ultimately result in a decline in software quality, an increase in iteration time, and thus a decline in one’s ability to meaningfully work on a project (other than purely maintaining its inertia—for instance, by fixing a leak reported by a customer).

One attempted solution to this problem found in the modern programming world is to introduce compiler and language features to automatically generate “inconvenient code”—in this case, that being the code responsible for correctly calling initialization and deinitialization code (which may include allocations and deallocations).

This is done in the C++ world through RAII—whenever an object goes out of scope, either by being initially allocated on the stack, explicitly allocated (in C++, through new and delete ), or by being within another object that is going out of scope, some code will automatically be called, which is responsible for cleanup (the destructor ). When an object’s lifetime starts , then some other code will automatically be called, which is responsible for initialization (the constructor ).

The constructor and destructor of an object mark the beginning and ending (respectively) of that object’s lifetime . The fact that this has the same overhead that malloc and free had is not relevant—it is purely trying to automate the generation of some code by assuming initialization and deinitialization are coupled with an object’s lifetime . This, of course, does not eliminate all possible bugs (misuse is still possible, and often not checkable in a language like C++)—so, this idea is often paired in newer languages with heavier (and more complex) compile-time checking features, which attempt to both automate this code generation, and prohibit misuse.

Another attempted solution is garbage collection , which is a large enforcement structure that tracks everything and interrupts productive work in order to perform its function (much like a government agency, except in this case, the garbage collector is ostensibly doing something approximating useful work—although both function by stealing valuable resources involuntarily). A garbage collector will periodically interrupt a running program—which is running normally—to explore the set of individual malloc -style allocations (objects) and find which of them are still being referenced somewhere in live data structures, thus detecting the termination of some allocation lifetimes , and being able to release those allocations. In many cases, garbage collectors do actually perform their function, although nevertheless it’s still possible to produce de facto leaks, by mistakenly holding an unnecessary reference to an object, which prohibits the garbage collector from releasing it.

The above solution attempts see the problem as fundamentally an automation or checking problem. It isn’t simply that memory management was being approached in an entirely wrong way—instead, the malloc and free rat’s nest is simply a part of the memory management problem’s intrinsic nature, and so tooling must simply aid the programmer in making fewer mistakes.

This view follows quite naturally from another aspect of modern programming thinking (and education), which claims many problems are gross and complex , and thus we need abstraction to make them appear simpler . It’s not, advocates of this philosophy claim, that the problems themselves should be simplified—they are, on the other hand, intrinsically complex , and it is the job of tooling to make them appear less complex.

I don’t agree with this view for a number of reasons. Firstly, complexity hidden by an interface (be it an API or a tool’s controls) does not simply disappear—it can be detected through performance problems, subtle bugs, and a lack of composability. Secondly, there is not a single user and a single producer in computing—the ecosystem is a complex graph of interdependent problems. There are “leaf nodes” in this graph, where produced software has no dependents—this would be, for instance, a game that never has any of its code reused. But the vast majority of the graph is producing software that must be composed with other software in unpredictable ways, and that production occurs by composing other dependency software in unpredictable ways. So complexity introduced at any point in this graph does not simply disappear—instead, it compounds . It is no coincidence that modern software—after many decades of cruft accumulation and software composition—seems to be slower, buggier, less reliable, and more frustrating to use than it should be.

Nevertheless, this view is dominant in the programming sphere at large, and as such is responsible for a host of solutions that don’t mind adding complexity to the problem.

My approach, on the other hand, is this: instead of assuming that malloc and free were the correct low-level operations, we can change the memory allocation interface —tweaking what the user and implementation agree on—to simplify the problem and eliminate many of the problems found in the traditional malloc and free style of memory management.

What, exactly, does that approach look like? To begin understanding it, let’s take a look at another style of memory management in C that does not have the same problems that malloc and free do: the stack.

As I mentioned earlier, the primary focus of criticism on memory management in C is on the malloc and free interface, and not the stack. That is for a good reason—using the stack correctly is remarkably simple. Misusing it is, of course, still possible. But after a new C programmer learns a few simple rules, it’s not particularly difficult to avoid almost all mistakes.

With stack allocation, the idea is simple: multiple allocation lifetimes—all using a single block of memory—may be in-flight at a single time, but the end of a lifetime may never cross the beginning of another lifetime. This means that several nested allocation lifetimes may exist, but it is not an entirely arbitrary timeline of overlapping allocation lifetimes (as in the case of malloc and free ).

This rule can be clearly visualized by looking at virtually any CPU profiler, many of which make use of what’s called a “flame graph”:

Each block in the above picture corresponds with one of these lifetimes. Once a lifetime has been entered, its parent lifetime cannot end until it first ends .

When a new lifetime opens—delimited by the { symbol in C’s syntax—that signifies a new “scope” on the stack. When a lifetime closes—delimited by the } symbol—the lifetime began by the corresponding { symbol is terminated. Any variables declared within those two symbols “belong” to that lifetime.

The syntax makes the rule of a lifetime’s end not crossing another lifetime’s beginning quite clear. The following is a valid case of multiple lifetimes:

But the following idea is not valid (and cannot even be expressed within the grammar):

The reasons why this cannot be expressed in C’s grammar are clear when considering the compiler tasked with parsing the above text. Because whitespace is insignificant in C, the first } encountered will be identified as closing lifetime B , and so it can not be interpreted as ending lifetime A .

The inability of the grammar to express that concept is ultimately irrelevant, though (you can imagine a language’s syntax that does make such a concept expressible)—what’s more interesting is the implications that this rule has on the “stack allocator”.

First what’s notable is that the concept of a lifetime has become detached from individual allocations, and is now delimited independently from allocations. When an allocation occurs (a variable is declared on the stack), its lifetime is chosen by virtue of which scope it is placed within. This is unlike malloc and free , which offers per-individual-allocation lifetime control. This allows an individual lifetime to be used to group many allocations into a single common case. Note that, additionally, this fits the “asymmetric allocation and deallocation” pattern I mentioned earlier—allocations can occur within a scope in sporadic ways, but all end together simultaneously (quite similar to the aforementioned “unloading a level in my game’s level editor” example).

Secondly, because of the rule that lifetimes within the stack may only exist entirely within other lifetimes, implementing a stack allocator is trivial. All you need is a single block of memory and a single integer:

To create a “sub-lifetime”, the “allocation position” ( stack_alloc_pos ) needs to simply be remembered before the sub-lifetime begins, and then it must be restored when the sub-lifetime ends:

You can also imagine “freeing” everything in the entire stack block, just by setting stack_alloc_pos to 0 .

The above is expressed in C syntax, but if you dig into what stack allocation actually means at the assembly level, you’ll find that C’s stack is implemented much like this. Notice how computationally trivial it is to perform both allocations and deallocations on the stack.

That is all well and good, and it’s great that the stack is so simple, and that it’s trivial to use it. But the stack is not an option in many cases—that is, after all, why malloc and free are often used as “the alternative”.

Why is the stack not an option in some cases, though ?

Let’s form an example where the stack would simply not work. Let’s start with the simplest example:

In the above image, I’m beginning a lifetime at the red { character, and ending that lifetime at the red } character. This clearly breaks the rules of the stack, because the lifetime I am attempting to form does not fit within the parent lifetimes I am attempting to begin or end the lifetime within.

Note that this lifetime can still be expressed in this timeline! I would simply need to begin the lifetime several ancestor scopes higher , and end the lifetime at that same level. But, doing that is often still not an option, because of composability .

In the below picture, the top red rectangle delimits one layer of code, and the bottom delimits another layer of code. Imagine that the top layer is some high-level application code, and the bottom layer is a helper library for parsing a file format.

Memory allocation is almost always coupled with the particular details of a task, and this is especially true in parsing, so in order for the application code (top) to call into the parsing code (bottom), the application code would need to do all of the memory allocation, somehow without doing any of the work involved in usefully using or filling that memory.

While that may be in principle feasible (even though in many practical scenarios, it’s not feasible), we can still form a case that is not theoretically feasible at all, by introducing another overlapping lifetime:

In this case, it’s impossible to keep the same order of events, while keeping both lifetimes in the same stack. In cases like this, the stack stops being an option.

But now, consider this: can you tackle the same lifetime problems as above, but with the ability to construct as many independent stacks as you need ? The answer being “yes, of course”—now, solving each problem is trivial. Some lifetimes must simply belong to different stacks than others.

This is the approach of the arena allocator: take the absurdly simple linear allocator, which offers lightning fast allocation and deallocation, eliminating per-allocation freeing requirements, first being proved out by the stack, and make that a formal allocator concept—the “arena”. Usage code can make as many arenas as necessary, and choose them at will for specific allocations.

An arena allocator’s fundamental API, then, may look like this:

Notice that, at the limit , this allocator becomes equivalent to malloc and free . To see this, consider that each malloc and free can simply be identified as beginning and ending their own little “stack”, where it’s only used for a single allocation. Using an arena in such a way—while it works—is not where this style of allocator will make an obvious difference.

The key point, I’ve found, is that in virtually every case , programs do not operate at such a limit. You don’t form a new scope for each new variable you declare on the stack—similarly, you don’t need an arena for each new allocation with a stack-breaking lifetime requirement. In almost every case, a large number of allocations can be bucketed into the same arena . And in those cases, once your arenas are set up accordingly, the requirement to deallocate any allocation disappears (other than the deallocation of the arena in its entirety, if required). Once you’ve performed an allocation, you’ve chosen an arena, and by virtue of that, the allocation’s corresponding memory will be made available again in accordance with the arena’s overarching lifetime.

By getting just a bit more organized about which arena we choose for an allocation, we’ve freed ourselves from the burden of free ing all of our dynamic allocations. We’ve also made it much easier to, for instance, track memory usage in our application, or bucket all allocations for a particular purpose, which may be useful for debugging or performance—we now have a fairly obvious path to determine which arena a given allocation is within , and to free all allocations in any arena we choose, irrespective of who pushed what onto the arena, when , and in what order .

A very high level description of an arena is “a handle to which allocations are bound”. When an allocation occurs, it is “bound” to an “arena handle”. This makes it easily expressible to, for instance, clear all allocations “bound” to an “arena handle”.

One useful property of arenas is that they gracefully propagate through several layers in a codebase. This occurs through the parameterization of codepaths with the arena they use to perform allocations.

It is trivial, then, to identify which functions are performing allocations. And because, in an API like the above, the arena is a required parameter, the caller must choose an arena, and thus determine the lifetime of any persistent allocations.

For instance, imagine that there is a file format which encodes a tree structure (like JSON, or Metadesk ). A parser for that file format will produce a complex tree structure, perhaps with pointers linking various tree nodes together. With arenas, that parsing API can be a function that takes (Arena*, String8) (Node) (where a single Node contains pointers to, for instance, its children nodes). The usage code passes an arena where it’d like the results allocated, instead of—for instance—having to carefully free the resultant structure at a later time. By choosing an arena, the freeing work is complete.

This simplifies all codepaths in this system. The parsing code becomes simpler, because it does not have to have any cleanup code whatsoever. The calling code becomes simpler, because it does not have to manage the lifetime of the parsed tree independently. And finally, the allocator code itself remains nearly trivial, and lightning fast.

Contrast this with a malloc -style interface propagated throughout the system. Because of the allocator’s assumption of arbitrary lifetimes and arbitrary sizes, the allocator code itself becomes more complex (it is a generic heap-style allocator); the calling code becomes more complex (it must manage the lifetime of the parsed structure); and the parser code becomes more complex (it must provide a careful freeing routine). So, this is not merely a choice —the arena solution is simpler and faster by every important metric .

As I’ve mentioned, a key concept behind the arena is grouping lifetimes together . But that, sometimes, is not as simple as it seems.

For instance, imagine I’ve allocated storage for 1,000 entities in my game’s level editor on an arena. But now, I’d like to go and remove one out of the middle. If each entity were malloc ’d, then all I would need to do is free the allocation for the entity in the middle. But if that storage is directly on an arena, how might that work?

Recall what “freeing memory” literally means —it means informing whatever allocator you allocated memory from that the memory you allocated is now available for future allocations, and you don’t plan on using anymore (at least, before allocating something new).

This desired behavior—of reusing memory that has been released—is still possible with an arena. To demonstrate the concept, I’ll provide a simple growable pool allocator implementation that composes with an arena .

A pool allocator only offers allocations of a fixed size, but with each allocation having an arbitrarily-different lifetime from all other allocations. So, it’s much simpler to implement than a fully generic malloc -style allocator, which makes it useful to demonstrate my point of “compositions with arenas”. That being said, keep in mind that malloc -style allocators can still be implemented as a composition with an arena—I’ve implemented a number of more sophisticated allocators as compositions with arenas, including malloc -style allocators, a quad-tree allocator, and others.

But in any case, here is what a simple pool allocator will look like:

Arenas are an extremely versatile building block. To aid in furthering understanding of just how versatile they are, I’ve gathered a number of scenarios—perhaps non-obvious to some readers—in which an arena clicks perfectly into place.

In games and graphical applications, programs are organized at a top-level by a loop. This loop performs the same operations repeatedly, in order to communicate to the user, and receive information from the user. On each iteration of this loop, a frame is produced.

It’s very common to have per-frame concepts in this scenario. For instance, you might be building a user interface every frame, or producing a batch of drawing commands. In such cases, it’s useful to have a lifetime that exists for the duration of a single frame. This can easily be implemented with an arena. The arena itself would be allocated permanently, but at the beginning of each frame, it has its allocation counter reset to 0 (e.g. via the ArenaClear API I described above). So, if in application code you’d like to dynamically allocate a few complicated strings for the purposes of rendering, you can simply use the frame arena, and know that the memory will be released at an appropriate time.

It’s also very common to refer to the previous frame’s state on any given frame. This can be done with a simple extension to the “frame arena” idea—instead of having a single frame arena, have two , and switch between them each frame (in effect, “double-buffering” the frame arena). This allows safe references to the prior frame’s state.

You may now see that the stack, as it is normally understood in C, is almost like a specialized per-thread arena. It’s very common to use the stack as a sort of “scratch arena”, by “pushing” (declaring) a temporary buffer in a local scope, and using it to do some useful work. While that is ultimately true, there are a few limitations of the stack that make it underpowered.

First of all, the stack is coupled with the call stack —so it’s not possible to, for example, push something onto the stack, and return back to a function’s caller, while keeping that allocation there (without making some brittle assumptions that inevitably break).

Secondly, the stack cannot compose with any code you’ve written that uses arenas. So if you’d like to, for instance, call a function with a signature like Node *ParseString(Arena *arena, String8 string) , there isn’t really a way to tell the function to use the stack —it is written to use arena .

This is where per-thread scratch arenas become useful. These are simply thread-local arenas, which can be retrieved at any time. They can then be used with the “temporary sub-lifetime” trick I introduced earlier, with an additional API layered over the core arena API:

The API to retrieve a scratch arena, then, can almost be the following:

Although the above is not quite sufficient. The reason why is quite subtle, but let me explain with an example:

Notice that, in the above case, if there is only a single per-thread scratch arena, both FunctionA and FunctionB grab the same scratch arena . The difference is that, in FunctionA , it is treating scratch.arena differently than arena . scratch.arena is an arena it’s using for temporary work that it expects to disappear before returning— arena is where it is allocating persistent results for the caller.

But, because scratch.arena is arena , the result memory is actually freed before returning to the caller.

So, another rule must be adopted. When GetScratch is called, it must take any arenas being used for persistent allocations, to ensure that it returns a different arena, to avoid mixing persistent allocations with scratch allocations. The API, then, becomes the following:

If only a single “persistent” arena is present at any point in any codepath (e.g. a caller never passes in two arenas), then you will not need more than two scratch arenas. Those two scratch arenas can be used for arbitrarily-deep call stacks, because each frame in any call stack will alternate between using a single arena for persistent allocations, and the other for scratch allocations.

This issue is, as I said, very subtle, and it does require an extra rule that makes scratch arenas slightly more complex to use correctly. But, the day-to-day habit of using scratch arenas is still not particularly difficult to follow: “If you want a scratch arena, and an arena is already in scope for persistent allocations, then pass it into GetScratch . If you introduce an arena parameterization into an already-written codepath, then find all instances of GetScratch and update them accordingly”.

As I’ll explain in the Implementation Notes , an arena implementation can have a strategy for growing and shrinking (while not relocating existing allocations). This makes them useful for storing collections of information where the size is not known upfront.

Depending on the exact growth strategy, this either makes arenas a suitable replacement for dynamic arrays, or a perfect implementation of one. Because the specifics of this style of arena usage are tightly coupled with implementation details, I’ll just mention this for now, and hopefully the Implementation Notes will illuminate the parts I’m glossing over at the moment.

I’ve already introduced the basic mechanism for implementing an arena, when I described how the stack works. An arena works in precisely the same way (although you can play with the details—for example, auto-aligning allocations).

There is one aspect I’ve not yet covered, however, which is what happens when the arena runs out of storage . There are a number of strategies one might employ in tackling this scenario, depending on their particular case.

One strategy is to simply pre-allocate a single fixed-size block of memory for an arena, and abort when storage runs out, because the project has strict memory usage requirements (if it’s, for example, on an embedded device).

The more common case, though, is that you’re writing code for, say, a modern consumer desktop computer, or a modern game console. In this case, it’s much easier to have a strategy for growing the arena’s storage when it runs out.

One strategy is to spread the arena across a variably-sized linked list of large blocks. If there is not enough room for a new allocation on an arena, your implementation falls back to asking the operating system for a new block, and then begins allocating on the new block. When the arena’s allocation position is pushed far back enough, it can return to the previous block. This notably, eliminates the guarantee of memory contiguity within the arena, which makes the arena unusable for implementing a dynamic array, because a dynamic array can be trivially accessed with a single offset into a single block. In this case, to access an arbitrary offset in the storage, you first would need to scan the chain of blocks. This is likely not prohibitively expensive, and the exact performance characteristics can be tweaked by changing the block size—but it does mean using it is more nuanced than just a single block of memory.

Another fancier strategy is to take advantage of modern MMUs and 64-bit CPUs (which are virtually ubiquitous these days). On modern PCs, for instance, it’s likely that you have 48 bits (256 terabytes!) of virtual address space at your disposal. This means, functionally, that you can still have arenas both grow dynamically, and work with just a single block (and thus maintain the guarantee of memory contiguity). Instead of committing physical storage for the entire block upfront (e.g. by using malloc for the storage), the implementation simply reserves the address range in your virtual address space (e.g. by using VirtualAlloc on Windows). Then, when the arena reaches a new page in the virtual address space range that has not been backed by physical memory, it will request more physical memory from the operating system. In this case, you still must decide on an upper-bound for your arena storage, but because of the power of exponentiation, this upper-bound can be ridiculously large (say, 64 gigabytes). So, in short, reserve a massive upper-bound of contiguous virtual address space, then commit physical pages as needed.

Finally, it’s easy to compose all of the above strategies, or make them all available in various scenarios, through the same API. So, don’t assume they’re mutually exclusive!

Before choosing any strategy, carefully consider which platforms you’re writing for, and the real constraints on your solution. For example, my understanding is that the Nintendo Switch has 38 bits of virtual address space. The magic growing arena can still work here, but you’ll have a tighter constraint on address space.

Learning to use arenas to organize lifetimes revolutionized my experience writing C. I almost never think about memory management anymore. It is not particularly more cumbersome than writing in a garbage-collected scripting language, but it required none of the performance or toolchain complexity costs, and it didn’t require me to sacrifice contact with the details.

Simple memory management doesn’t take a gigantic, complex compiler. It doesn’t take a garbage collector. And it doesn’t require sacrifice of contact with lower level details. Instead, it arises through a harmony between lower and higher levels. For this reason, I reject the modern programmer’s conception of “high-level vs. low-level”, and how “lower level details” are considered “gross”, inconvenient, and annoying. Lower level details are nothing to shy away from. They’re a necessary part of the problem’s reality, and by giving them their dues, code can benefit at both high and low levels.

If you enjoyed this post, please consider subscribing. Thanks for reading.

-Ryan

Discussion about this post

Ready for more?

Political meddling at the Census Bureau damages the US statistical system

Hacker News
www.piie.com
2026-09-06 18:36:07
Comments...
Original Article

This summer marks the worst period for US statistical integrity since President Donald Trump fired the head of the Bureau of Labor Statistics last August. Two different and seemingly uncoordinated events at the Census Bureau raised alarms about inappropriate political encroachment on the scientific and technical authority of the agency.

I take some comfort that journalists and independent experts spoke up quickly and loudly. I still trust the economic indicators we get from the expert staff at Census and other US government statistical agencies, like inflation data and the jobs report. But I am scrutinizing any technical changes more closely and am quicker to look for political influence.

During this administration, the statistical system has suffered attrition, leadership vacancies, cuts, delays, and political interventions in methods and content . Most of these developments are the result of collateral damage rather than intentional harm . But the examples of deliberate political intervention have grown, including these latest events at Census.

These two events—the publication of a brief on the official Census platform about noncitizen voting that fell far short of Census standards and a political decision about privacy-protection methods—are new harms. The main impact is likely to be on demographic data for voting and redistricting, and on local demographic and economic data. The headline-worthy macroeconomic data that policymakers, financial markets, and the media follow closely were spared this time around. But damage to any part of the statistical system reverberates throughout the system. These events demonstrate that the Census Bureau and other agencies are increasingly vulnerable to political interference that would harm their credibility and the ability of businesses and policymakers to make the data-driven decisions that affect Americans' daily lives.

The noncitizen voting brief

The clearer case of political encroachment was a brief published by the Census Bureau on August 18 that "readily determined with high confidence" that more than 24,000 voter records in the 2020 election represented noncitizens. The brief is formatted like other Census research briefs and reports but—in a departure from standard practice—named no authors and included no acknowledgements or contact information for any staff or team. NPR reported that the analysis was not conducted by career Census civil servants and that the team that generated the brief included individuals affiliated with the Trump-aligned America First Policy Institute.

The analysis linked a commercial voter file containing registration and 2020 turnout records to federal administrative records containing information that can be used to classify people as noncitizens, drawn from multiple sources including Social Security and immigration data. John Abowd, the former head of research and methodology at Census, wrote that previous Census evaluations of its record-linkage system found false-match rates high enough that the true number of noncitizen voters is probably much lower: "[T]he alleged 'noncitizen voters' are therefore much more likely to be data errors than noncitizen voters." Amy O'Hara, who formerly led Census research on administrative data, wrote that the report "lacks methodological rigor, transparency, and accountability, and the results should be discarded. Moreover, the Census Bureau should answer for the process concerns raised by the publication of this unsigned and shoddy report."

This looks bad! All administrations publish data to support their policy goals or political arguments. But authorship and accountability must be clear. Typically, administrations advocate for their views through offices directly led by political appointees. Presenting this report as a traditional Census publication does not make the analysis more credible. Rather, it undermines confidence in the Census Bureau and in the vast majority of Census products that do use appropriate methodologies, rigor, and transparency.

Ending noise infusion

The subtler case of political encroachment surfaced publicly in early June. The Commerce Department issued a department administrative order to prohibit the use of "noise infusion," one of the technical methods statistical agencies use to protect the privacy of individuals, households, and businesses represented in published statistics. From now on, the Census Bureau and the Bureau of Economic Analysis (BEA), which are both part of Commerce, can no longer use noise infusion, which adds random perturbations (or "noise") to data values. Acting Census Director George Cook, a political appointee, explained that using noise infusion "led to concerns that adding noise made the statistics less accurate and less trustworthy" and the administration's "top priority is to provide the public with accurate statistical information." He wrote that the policy was developed by the Commerce Department "with technical guidance from the U.S. Census Bureau." Census and BEA can continue to use other methods of privacy protection such as coarsening (broadening reporting categories, such as states instead of counties, or ten-year age buckets instead of five-year buckets) or suppression (not publishing some data at all).

Noise infusion has advantages over coarsening and suppression: Noise infusion can make it more feasible to publish data for small geographic areas and narrowly defined groups. With coarsening, detail like racial categories, age groups, or small geographic areas could get combined; with suppression, data for small groups might not get published at all. Therefore, with coarsening and suppression, the loss of data detail and availability is more concentrated on particular groups or areas than with noise infusion, which can spread accuracy loss more thinly across many cells rather than eliminating or aggregating particular cells.

Census had long used noise infusion in some economic datasets and expanded its use recently, most prominently though controversially, for the 2020 Census, through an approach called differential privacy, a mathematical framework for measuring and setting a level of privacy protection. With differential privacy, noise can be calibrated to meet the desired privacy standard while preserving as much accuracy as possible in the published data. In the debate over differential privacy, its supporters argue that it provides a measurable privacy guarantee while preserving more detailed data than other privacy protection methods do. Some critics have objected that adding noise degrades accuracy, though in fact all disclosure avoidance methods—including coarsening and suppression—involve a tradeoff between privacy and accuracy, availability, or both. Other critics have objected in recent years to Census setting too stringent a privacy standard, to overstating the risk of disclosure, and to the difficulty data users have in understanding differential privacy.

In response to the Commerce Department order, Census delayed the release of the annual American Community Survey (ACS), due in September, to give itself more time to incorporate alternative privacy protection methods, which could lead to suppressing some statistics or publishing less detailed data. Economic data like County Business Patterns and, eventually, Quarterly Workforce Indicators might get cut back or lose granularity, since Census has used noise infusion for those datasets for many years .

A key issue is whether the choice of privacy protection methods is a policy decision, and therefore appropriate for political appointees at the Commerce Department to make, or a technical decision, and therefore within the authority of career civil servants with relevant technical background to make. I share the view of several participants at a session on disclosure avoidance with Census Bureau leadership at the annual Joint Statistical Meetings, co hosted by the American Statistical Association and other organizations, in August: The balance between privacy protection and data availability and quality should be a policy choice, and appropriate for an administration or Congress to determine, but the choice of tools used to achieve that balance is a technical choice for career civil servants to make, with extensive public consultation. It is not appropriate for political leadership at the Commerce Department to take noise infusion off the table—and certainly not via an administrative order issued without a public notice-and-comment process.

The common threads

Two common threads link the seemingly unrelated cases of the noncitizen voting brief and the department administrative order to end noise infusion: They both involve political encroachment on decisions and processes that have historically been the domain of scientific and technical experts who are career civil servants, and they both appear to support known administration political goals .

During the Trump administration, there has been significant attrition of senior career civil servants at the Census Bureau and many other agencies. Ron Jarmin retired as Census deputy director earlier this year; he worked at the bureau for over 30 years and was in recent years arguably the most influential career official in the US statistical system. The Census chief scientist position is currently vacant, with an acting associate director temporarily in the seat. Other senior leaders have resigned or retired.

As a result, Census has lost important career leadership and institutional knowledge, potentially reducing the agency's capacity to resist inappropriate political pressure. In contrast, career staff pushed back in early 2021 when political appointees pressured them to rush a report on noncitizens, which staff warned could be "statistically indefensible" and potentially harmful to Census's reputation . The two events of 2026 suggest that the agency's capacity to resist inappropriate political pressure has weakened.

The events also align with administration political aims. For the noncitizen voting brief, the connection is direct, as Trump has repeatedly alleged without credible evidence that widespread noncitizen voting occurred in the 2020 election, providing reason to question or deny his loss in the presidential election. On January 6, 2021, President Trump claimed that "over 36,000 ballots were illegally cast by non-citizens" in Arizona. More than five years later, on August 18 on Truth Social, President Trump posted the numbers from the noncitizen voting brief Census published that day and exclaimed "I won the election!"

The connection between Commerce's noise infusion order and administration political goals is less obvious. As noted above, the department said its goal was better data accuracy. But because it is unusual for a methodological decision like banning noise infusion to be made at the political level, it helps to put this order in context of other recent news.

One possible effect of relying on coarsening and suppression instead of noise infusion is less detail about race and ethnicity, especially for small geographic areas, in the redistricting datasets derived from the 2030 Census: This possibility was discussed at the meeting between statisticians and Census leadership.

A recently leaked proposal under discussion makes this speculation plausible. It was reported that the administration has circulated a draft regulation that would bar questions about race, ethnicity, and sexual orientation from the 2030 short-form decennial Census; danah boyd, whose ethnography of the 2020 Census is being published next month, points out that without race and ethnicity data it becomes even harder to challenge potentially discriminatory maps under the narrowed scope for Voting Rights Act redistricting challenges. This proposal and Commerce's order on noise infusion could have similar, reinforcing effects on the collection and publication, respectively, of racial and ethnic data.

There is also a link between the administrative order and long-standing efforts by Trump and allied groups to obtain and use more detailed citizenship data from the decennial census. The Center for Renewing America , the think tank founded by current Office of Management and Budget director Russell Vought, makes the policy link explicit in two documents. First, an August 2025 explainer about differential privacy says, under the heading "citizenship nullification": "Even if the citizenship question is added to the Census, it will be impossible to ascertain the status of individuals so long as differential privacy is used. The algorithm will be able to mask characteristic data, including citizenship status, preventing the ability to determine total voter eligibility or how to draw maps to exclude those populations." [1] Second, the think tank's November 2025 primer on redistricting recommends "eliminating differential privacy," spelling out that differential privacy "undermines the precision necessary for redistricting, especially when drawing citizen-only districts."

Therefore, the think tank describes differential privacy as an obstacle to excluding noncitizens in the post-2030 redistricting process. If a citizenship question is on the 2030 Census, differential privacy could infuse noise into published citizenship data for small geographic areas, making those data less suitable for implementing citizen-only redistricting.

What's the damage?

Neither the noncitizen voting brief nor the Commerce order on noise infusion alters the calculation of headline macroeconomic statistics like job growth, unemployment, inflation, or GDP. These headline macroeconomic data come from the Bureau of Labor Statistics, which is part of the Labor Department, not Commerce, and the BEA, which is part of Commerce but conducts relatively few surveys and relies primarily on inputs from other agencies for its most-watched statistics. Furthermore, most Census economic indicators are already protected primarily through suppression and are not expected to be affected ; the administrative order affects demographic surveys more than economic indicators.

And yet there are immediate and real costs from these episodes, which can snowball. Data users will scrutinize official data releases and reports more closely. We need to check more carefully who is credited and who is accountable for statistical agency releases. We need to watch carefully, when the 2025 American Community Survey eventually comes out, whether the ban on noise infusion leads to less data being published and which tables are suppressed. We also need to question seemingly technical methodological changes and scrutinize them for political motivation more closely. It is rational and necessary for data users to be more on guard.

Unfortunately, greater scrutiny by data users creates additional burdens for the statistical agencies themselves. When data users rightly feel that they need to wonder if seemingly technical decisions might have been politically motivated or influenced, the statistical agencies have to explain and defend their decisions more carefully to preserve trust. Twice this year, the BEA has announced changes to how specific components of its inflation measure, the Personal Consumption Expenditures (PCE) Price Index, are calculated. The first change—to legal services prices—resulted in lower inflation than expected when it was incorporated without advance notice in the report for January. A second set of changes to multiple price components are expected on net to lower inflation when they are incorporated in the report for August.

There is no evidence that either change was politically influenced or anything other than a reasonable methodological improvement. But sophisticated data users criticized the lack of transparency around both decisions: The first was " not a good look " according to Omair Sharif, founder of a leading inflation forecasting firm, and the second " raised some eyebrows " according to Matt Grossman's reporting in the Wall Street Journal . In this sense, political interference anywhere in the statistical system raises the burden of proof for the entire statistical system. That data users are more on guard is not the BEA's fault, but it has become its problem. To maintain trust, the statistical agencies now need to telegraph and justify their technical decisions more carefully than before, and that diverts their shrinking resources from other efforts.

These dynamics threaten badly needed modernization and innovation to the statistical system, to deal with declining response rates and to take advantage of richer private and administrative data. Agencies that have to spend resources to defend methodological challenges from general growing suspicion of political influence might, on the margin, decide against making necessary investments that may be fully justified on technical grounds but might happen to result in politically useful statistics or data products. Congress might, on the margin, reduce funding for the agencies if they doubt the integrity and independence of the agencies. Businesses and households might become even less likely to respond to government surveys if the statistical agencies are less trusted. All of this would make the agencies' jobs harder and fuel more attrition—which in turn raises the risk of more political encroachment.

Damage to the integrity of the statistical system is accumulating, with the disappearance of datasets at the start of this administration , last August's firing of Bureau of Labor Statistics commissioner Erika McEntarfer, and the repeated misrepresentation of economic data to support false claims. To repeat: There's no indication that headline economic data can't be trusted. The United States is not falsifying or suppressing macroeconomic statistics in the ways some other countries have done, with disastrous results. There is no evidence we are close to that point. But the list of warning signs has grown, and the need for greater data-user vigilance and scrutiny imposes real costs on the statistical system.

Note

1 . Differential privacy doesn't actually prevent Census from knowing an individual's confidential information like citizenship status; it affects what can be publicly inferred from published aggregates.

Blockstream pauses Liquid Network after attackers claiming to be whitehats take 4,000 BTC (~$320 million)

Web3 Is Going Great
web3isgoinggreat.com
2026-09-06 18:19:21
An unauthorized withdrawal of 3,998.5 BTC (~$320 million) from the Liquid Network, a bitcoin sidechain, prompted a network halt. By disabling nodes that bridge between Liquid and the bitcoin mainchain, attackers are limited in their ability to cash out via bridge. Blockstream, the developer...
Original Article

An unauthorized withdrawal of 3,998.5 BTC (~$320 million) from the Liquid Network, a bitcoin sidechain , prompted a network halt. By disabling nodes that bridge between Liquid and the bitcoin mainchain, attackers are limited in their ability to cash out via bridge. Blockstream, the developers of Liquid Network, also said they had contacted exchanges to ask them to pause LBTC deposits and withdrawals, cutting off another avenue.

The unauthorized transaction included a message reading "we are whitehats . contact us on chain", suggesting the possibility that the withdrawal was in fact well-intentioned security researchers aiming to "rescue" funds after discovering they were vulnerable and then return them to a secure wallet. However, as of the afternoon on September 6, Blockstream had only said that they were "working on contacting" the "purported white-hat hackers".

Data races and the limits of ThreadSanitizer in C and Go

Lobsters
theconsensus.dev
2026-09-06 18:13:09
Comments...
Original Article

To gain confidence in code we write tests. But when you add concurrency to the mix the bugs become nondeterministic. To gain additional confidence in concurrent code we might enable a race detector that tells us that a race happened in some piece of code during a run. This is useful because our tests might still have passed even though a race happened. The existence of the race, whether our tests otherwise fail or not, means that a latent bug absolutely exists.

Most major language implementations that have any race detector (Clang, GCC , Go , Swift , OCaml ) use LLVM’s ThreadSanitizer . ThreadSanitizer (TSan) is not well documented. It has gone through three major iterations and while you can find the algorithm for TSan version two ( released in 2012), the author of TSan suggests we just read the source to understand version three ( released in 2021). Perhaps someone will contribute new docs.

There is a rich history of algorithms for detecting data races like Eraser (which influenced TSan version one), FastTrack (has ideas in common with TSan versions two and three), RaceTrack by Microsoft, and so on. Each algorithm has its own limitations, as TSan does too. And Clang, GCC, and Go do emit generic entry points for race detector libraries, even if TSan is the only one in serious use today.

In this article we’ll walk through the basics of data races. Then we’ll implement an idealized interpreter in Python for multi-threaded C code alongside FastTrack-style vector clocks to show how TSan roughly works. Then we’ll show how architecture choices make it possible to overload TSan in a few dimensions, causing it to miss obvious data races. For example, TSan cannot reliably report data races while crossing a 255 total thread boundary. This is not a particularly rare situation, considering web services implemented in a language like Go with one goroutine per request.

Want to keep reading?

The Consensus is a bootstrapped company that depends on your support to produce articles like this.

Join the free weekly newsletter and we'll let you know when it's out, along with new jobs and funding in software infrastructure.

Or, subscribe or sign in for immediate access.

This article will be free to read in two weeks.

Law couple develop purpose. Surface school keep per. Class race player ground only allow poor. My use candidate back one whether including commercial. Up performance environment. Artist watch on our option beat news. Feel land become. Cause center nice moment thousand speech exist. Raise I table center break.

Particularly ground local. Maintain wish bed pick in. Return stage fear but cost resource. Popular question receive relationship add right. Idea effect top improve that chance four you. Specific believe hand collection. Energy although loss everybody. Statement something lawyer help magazine example. Herself through bill foot instead more. Analysis season or too myself beautiful could. Method newspaper evening.

That become success office. Decade pretty couple school. Face candidate skill back pay rule. Feeling policy nation. Well across wish teach build treatment commercial. Spring kid participant accept work within level. Soon trip somebody oil story. Authority with among traditional area. Life as action media question. Tree west significant plant.

Leave low quality issue. Next someone similar lot. Serious entire less place ever middle what act. Build improve somebody order or article. Election seven thank play interview condition. Foreign our entire. Building all choose they idea area. Issue focus note. Mother last door close sit.

Noticed a mistake? Have a question or comment? Write to the editor .

Windows 11's "special" developer edition looks like another marketing misfire

Hacker News
www.neowin.net
2026-09-06 17:16:10
Comments...

Signing TLS handshakes inside a TPM

Hacker News
bschaatsbergen.com
2026-09-06 17:05:21
Comments...
Original Article

I’ve been doing remote attestation work on confidential VMs. This post is one piece of that rather than the whole of it: wherever the attestation itself lands, the machine still needs an identity it can hold and use afterwards, and in my case using it means authenticating to other services with mutual TLS.

An application that does mutual TLS authenticates with a client certificate, and on disk that is almost always two files. client.crt is the one you show: it carries a public key and a CA’s signature over that key, and it isn’t a secret. client.key is the one you sign the handshake with, and the server checks that signature against the public key in the certificate you just showed it.

So the identity is entirely in the second file, and your process reads it into memory at startup. From that moment the key is in a heap dump, a core file, a swapped page, a hypervisor snapshot of the VM’s memory, and in reach of anything that gets code execution in the process. Copy it and you are that machine everywhere that machine is trusted: the secret store, the internal API that only accepts client certificates, the database that maps a certificate subject to a role.

Which is the part I keep coming back to. A key in a file is not really a machine identity. It’s a bearer credential that happens to be stored on a machine, and whoever reads it becomes that machine, anywhere, until somebody notices and revokes the certificate.

Let me be specific about the threat, because I think this gets oversold. What I’m defending against is an attacker who gets read access inside the guest: a file read, a core dump, an SSRF that reaches the filesystem, a backup that went somewhere it shouldn’t. I want them to leave without a working copy of the machine’s identity.

What I’m not defending against is someone with persistent code execution using that identity while they’re still on the box. That’s a different problem and a TPM doesn’t solve it.

So the thing I need has four properties. It signs TLS handshakes, because that is how the identity actually gets used. It never exists outside the machine, so that reading memory or a disk gets an attacker nothing they can carry away. It needs no second credential to reach it, because whatever that credential was would immediately become the new thing worth stealing. And it works with an ordinary TLS stack, because I don’t want a bespoke protocol between two services that already speak TLS perfectly well.

A TPM does all four. It will sign with a key and it will not hand the key over, so the process never holds a secret at all. On the machine I’ve been using that’s a virtual TPM, and I wrote about the chip itself in an earlier post . Being a confidential VM helps with exactly one line of that exposure list, since guest memory is encrypted against the host and a snapshot taken from underneath gets ciphertext. Everything else on the list happens inside the guest, where it does nothing at all.

why not something else

Three alternatives come up before a TPM does, and all three are reasonable. Run each against those four properties and you can see what each one misses.

File permissions are worth tightening and fail the second property. SELinux, RLIMIT_CORE=0 and mlockall genuinely close vectors, but a process that uses the key has the key in its address space, and ptrace , /proc/pid/mem and code execution in that process are all untouched by the mode bits on a file that has already been read.

Short-lived certificates fail the second property too, and partly the third. They shrink the window a stolen key is useful in, which is valuable and probably more valuable than this post if you can only do one. They don’t stop the copy, and you still need some credential to authenticate the renewal.

A KMS is the real competitor. Cloud KMS or Vault’s transit engine holds a key that genuinely never leaves, so it passes the second property outright, and for a lot of teams it is the right answer. What it fails is the third. A KMS has to authenticate you somehow, so there is now a credential sitting on the box that unlocks the signing key, and that credential is the thing an attacker copies instead. You have moved the problem rather than removed it, unless that credential is itself bound to hardware.

There’s a practical cost on top of that, which is that every cold handshake now depends on a network round trip to a service that can be down. I would rather not put a remote dependency in the path that decides whether two of my services can talk to each other.

That’s the third property, and it’s the one that decides this. Something has to be the root: held by the machine, unstealable, and reachable without presenting anything first. A TPM is that thing, and everything else here (a KMS token, a short-lived workload certificate, a PKCS#11 token you’d have to go and buy) can hang off it rather than compete with it.

None of which makes a TPM more secure than a well-run KMS. It’s local, it needs no credential to reach, and on most machines built in the last decade it is already there.

what TLS asks of a key

Sign, and nothing else, is all a TPM will do with a key. That happens to be all TLS needs.

In TLS 1.3 a peer proves it holds the private key for the certificate it just sent by signing a message defined in CertificateVerify . That message is 64 bytes of 0x20 , the string TLS 1.3, client CertificateVerify , a zero byte, and then the transcript hash, which is a running hash of every handshake message the two sides have exchanged so far. All of it gets hashed, and that digest is what the key signs. A server authenticating with a certificate signs the same structure with server in place of client , which is what stops either side’s signature being replayed back at the other.

Two things follow from the transcript being in there. Because it includes fresh randomness from both peers, the signature is good for this one connection and no other, so it can’t be replayed. And because there’s one transcript per handshake, the key is asked for exactly one signature over one digest.

That’s the whole job. The private key never decrypts anything and never derives the session keys. Those come out of an ephemeral Diffie-Hellman exchange it takes no part in. Once CertificateVerify is on the wire, the key is done for the life of the connection.

Go asks for exactly that much and no more. A tls.Certificate holds a PrivateKey , and what that field actually has to satisfy is crypto.Signer :

type Signer interface {
	Public() crypto.PublicKey
	Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error)
}

crypto/tls builds the CertificateVerify message itself, hashes it itself, and calls Sign with the digest. It never asks for key material. This is the same door PKCS#11 modules and cloud KMS keys have always come through, which is why I said above that they all look alike from Go’s side. It was never a TPM feature. The interface is just narrow enough that a TPM fits through it.

Now compare that to TPM2_Sign : digest in, signature out, key stays put. Same shape. So the library I ended up writing is a Sign that forwards to the TPM, plus some care around attaching to the right key.

process memory TPM crypto/tls hashes the transcript key TPM2_Sign digest signature
Only the digest and the signature cross. The key never does, so a heap dump, a core file, or a swapped page has nothing to leak.

the client

Here’s the whole client. The thing to notice is that nothing in it names the key:

leaf, err := x509.ParseCertificate(certDER)
if err != nil {
	return err
}

// The certificate picks the key.
key, err := tpmtls.OpenForCertificate(tpmtls.DefaultDevice, leaf)
if err != nil {
	return err
}
defer key.Close()

conn, err := tls.Dial("tcp", "store.example.com:443", &tls.Config{
	MinVersion:   tls.VersionTLS13,
	Certificates: []tls.Certificate{key.TLSCertificate(leaf.Raw)},
})

There’s no handle, no key path, and no device configuration beyond a default. The tls.Config doesn’t mention a TPM either. It’s the same config you’d write for a key in a file, and it works just as well in an http.Transport , in tls.Listen , or on an http.Server .

OpenForCertificate comes from go-tpm-tls , a thin layer I wrote over go-tpm and go-tpm-tools . That it takes the certificate and nothing else is the one design decision I went back and forth on, so it’s worth explaining.

A key in a TPM sits at a handle, which is a number like 0x81000004 saying where the object lives. The obvious thing is to put that number in configuration, and I wrote it that way first. It’s wrong. The handle is chosen by whoever provisioned the key, so a value that works on one machine fails on the next, and it fails at the first handshake rather than at startup, which is the worst possible time to find out. But your application already holds the certificate it’s about to present, and the public key inside that certificate says exactly which key in the TPM to sign with. So the lookup reads the persistent handles, compares public keys, and picks the match. If nothing matches you get tpmtls.ErrNotFound at startup.

It also fits how the two halves age. The key stays in the TPM for the life of the machine and the certificate over it is what rotates, so the certificate is what your application gets handed fresh and the key is what it has to go and find. If you do know the handle, tpmtls.Open takes one.

Two smaller things in that snippet. tpmtls.DefaultDevice is /dev/tpmrm0 , the kernel resource manager, and you want that rather than the raw /dev/tpm0 because it gives every open file descriptor its own context, so processes sharing the TPM don’t evict each other’s objects. Hold onto that per-descriptor detail, it comes back in the benchmarks in a way I didn’t expect. Opening it needs root or membership of the tss group.

And you should pin MinVersion: tls.VersionTLS13 rather than inheriting Go’s default of 1.2, though I want to be precise about why, because the obvious reason is mostly obsolete. In TLS 1.2 a server certificate’s key can be asked to decrypt a premaster secret under the static RSA key exchange suites, and a TPM signing key cannot do that. Modern Go already puts those suites in its disabled list, and client authentication in TLS 1.2 is a signature either way, so this is not a live risk so much as one fewer thing to reason about. Pin it because TLS 1.3 makes signing the only thing that can ever be asked of the key, and because it is what the numbers later in this post were measured on.

It works, but it rests on six things being true, and only one of them is yours.

what the code assumes

Five of the six were decided before your process started, which is the point I made at the top: the security property is set at provisioning time and your code is attaching to the result.

A signing key already exists in the TPM. Something else put it there, typically an attestation agent that generated the key inside the TPM, bound the public half into hardware evidence, and had it certified. go-tpm-tls has no way to create a key, deliberately. It also won’t evict one. Close lets go of the key and leaves the handle alone, because detaching from a key you didn’t provision shouldn’t destroy it for everyone else on the machine.

The key is unrestricted. A restricted key, which is what an attestation key is, will only sign data the TPM itself hashed or produced. TPM2_Sign wants a validation ticket proving that, and a transcript hash handed in by crypto/tls has no such ticket, so the TPM answers TPM_RC_TICKET . If your agent provisioned an attestation key and a TLS key, you want the second one.

The key is ECDSA, on P-256 or P-384. RSA keys load and sign, but not the way crypto/tls asks, which I’ll come back to.

The key has no auth value. go-tpm-tls loads with a nil session, which resolves to a null session, so a key guarded by a password or a policy will not load. That is a real limitation and not a security claim: an auth value would be one more thing an attacker on the box has to obtain, and if your provisioning sets one, this package is not what you want yet.

The key sits at a persistent handle. Persistent handles run from 0x81000000 to 0x81FFFFFF , survive a reboot, and can be reached by whatever process comes along later. A transient handle works too, but only over the TPM connection that created it, and it costs about ten times as much per signature. If you have any say in provisioning, ask for persistent.

Your process can open /dev/tpmrm0 . This is the one that’s yours, and the only one you can fix without re-provisioning the machine. It needs root or the tss group.

Since you inherit the other five, I’d check what you actually got rather than trusting whatever runbook provisioned it. NonExportable reads the attributes back out of the TPM:

ok, err := key.NonExportable()
if err != nil {
	return err
}
if !ok {
	return fmt.Errorf("key at %#x can be duplicated out of the TPM", key.Handle())
}

That’s the fixedTPM attribute, which is the TPM telling you it will refuse to duplicate the private key to another TPM. Paired with sensitiveDataOrigin at creation time, which says the TPM generated the key rather than being handed one, you know the key has never existed anywhere else. NonExportable only reads the first of those two back; the second is a property of how it was provisioned.

From a shell it’s tpm2_getcap and tpm2_readpublic , worth running once so you know what the good case looks like. This is a GCE SEV-SNP guest with tpm2-tools 5.6:

$ sudo tpm2_getcap handles-persistent
- 0x81000004

$ sudo tpm2_readpublic -c 0x81000004
name: 000ba090006025e535419d16b12e7bdd219d20077aa4bd0fc99bc41cc5ed643005b3
name-alg:
  value: sha256
attributes:
  value: fixedtpm|fixedparent|sensitivedataorigin|userwithauth|sign
  raw: 0x40072
type:
  value: ecc
curve-id:
  value: NIST p256
scheme:
  value: ecdsa
scheme-halg:
  value: sha256

fixedtpm and sensitivedataorigin are the two that matter, sign without restricted is the third, and there is no private half anywhere in that output because there is no way to ask for one.

making a key to try this with

On a real machine an agent creates the key, but you need one to run any of the above, so here’s the short version.

Read the warning before you copy it. This makes a key that cannot leave the TPM, and that is all it does. Nothing binds it to what the machine booted, and no verifier anywhere has evidence of where it came from, so a certificate issued over it attests to nothing beyond “some TPM held this”. That is fine for running the code in this post and it is not provisioning. A real agent creates the key, binds the public half into a quote, and has it certified against that evidence, which is the part that makes anyone’s trust in it justified.

With that said, client here is go-tpm-tools/client :

// A throwaway key for trying out the code in this post.
template := tpm2.Public{
	Type:    tpm2.AlgECC,
	NameAlg: tpm2.AlgSHA256,
	Attributes: tpm2.FlagSign | tpm2.FlagSensitiveDataOrigin |
		tpm2.FlagUserWithAuth | tpm2.FlagFixedTPM | tpm2.FlagFixedParent,
	ECCParameters: &tpm2.ECCParams{
		Sign:    &tpm2.SigScheme{Alg: tpm2.AlgECDSA, Hash: tpm2.AlgSHA256},
		CurveID: tpm2.CurveNISTP256,
	},
}

const handle = tpmtls.Handle(0x81000004)

rwc, err := tpm2.OpenTPM(tpmtls.DefaultDevice)
if err != nil {
	return err
}
defer rwc.Close()

// Creates the key inside the TPM and persists it at the handle.
created, err := client.NewCachedKey(rwc, tpm2.HandleOwner, template, handle)
if err != nil {
	return err
}
created.Close() // let go of it; the key stays at the handle

That template is five of the six assumptions written out in code, which is a nice way to see them. FlagSensitiveDataOrigin means the TPM generates the private key rather than being handed one. FlagFixedTPM and FlagFixedParent mean it can’t be duplicated out. FlagSign with no FlagRestricted means it will sign a digest you give it, which is what CertificateVerify needs. The curve is P-256, NewCachedKey persists it rather than leaving it transient, and since no auth value is supplied the key ends up with an empty one, which is what lets go-tpm-tls load it with a null session.

You still need something to present, and key.CertificateRequest produces a CSR signed by the TPM that you can hand to whatever CA you’re testing against.

Evict the key when you’re done. Persistent slots are few and a key left behind survives reboots:

tpm2.EvictControl(rwc, "", tpm2.HandleOwner, handle, handle)

Owner auth is the empty string on the confidential VMs I ran this on. That is not true everywhere.

the other end doesn’t know

Here’s the verifying end. There’s no TPM on this side and nothing here knows about one:

pool := x509.NewCertPool()
pool.AppendCertsFromPEM(caPEM)

srv := &http.Server{
	Addr: ":8443",
	TLSConfig: &tls.Config{
		MinVersion: tls.VersionTLS13,
		ClientAuth: tls.RequireAndVerifyClientCert,
		ClientCAs:  pool,
	},
}

The server checks a signature against the public key in a certificate that chains to a CA it trusts, and where the private half was sitting while that signature got made is invisible to it. I think that’s the most underrated part of this whole approach, because it means you can adopt it one service at a time, from either direction.

Nothing above is client-specific, incidentally. The server’s own key is a crypto.Signer too, so putting it in the TPM is the same swap and the same call:

key, err := tpmtls.OpenForCertificate(tpmtls.DefaultDevice, serverLeaf)
if err != nil {
	return err
}
defer key.Close()

srv.TLSConfig.Certificates = []tls.Certificate{key.TLSCertificate(serverLeaf.Raw)}

A server signs one CertificateVerify per full handshake exactly like a client does, so everything later in this post about cost applies unchanged, and the throughput ceiling matters a great deal more on a listener than on a dialler. Its clients need no changes at all, because from their side this is still an ordinary certificate.

It’s also the limit, and it’s the thing I see people get wrong when they describe this in a design review. A TPM changes how strong the possession claim is. It does not change what the far end verifies. If you want the verifier to know that the key is in a TPM, and which machine’s TPM, that has to be carried in the certificate, which means it has to come from attestation at issuance time. The handshake conveys none of it.

four ways it breaks

Four things broke while I was getting this working. Every one of them produces an error that mentions neither TLS nor the TPM, which is what made them annoying, so I’ve listed them by the message you’ll actually see.

salt length must be rsa.PSSSaltLengthAuto

You’re using an RSA key. TLS 1.3 requires RSA-PSS with a salt as long as the digest, so crypto/tls asks for exactly that. A TPM picks its own salt length and won’t be told otherwise, so go-tpm-tools refuses the request rather than produce a signature the peer would reject.

The key loads fine, so this fails at signing time in the middle of a handshake, in a message that mentions none of the above. Use P-256 or P-384 and the problem doesn’t exist.

key at 0x81000004 cannot sign: restricted keys are not supported

You attached to a restricted key, almost certainly the attestation key. It signs only what the TPM itself hashed, which it proves with a validation ticket, and a TLS transcript hash is caller-supplied and has no ticket.

You can watch this happen below go-tpm-tls. Hand a restricted key 32 random bytes and ask it to sign them, which is structurally what CertificateVerify does:

$ sudo tpm2_sign -c restricted.ctx -d -g sha256 -s ecdsa -o sig.bin digest.bin
ERROR: Esys_Sign(0x3E0) - tpm:parameter(3):invalid ticket
ERROR: Unable to run tpm2_sign

The same command against an unrestricted key signs those bytes happily. TPM_RC_TICKET is the refusal that go-tpm-tools is reporting to you when it says restricted keys are not supported.

This one fails at load, and that’s deliberate. The check runs when you attach rather than when you handshake, so a misconfigured key takes down startup instead of one connection an hour later.

tpmtls: no persistent key matches

That’s ErrNotFound , and it means the certificate you’re holding and the contents of the TPM disagree. Either the agent hasn’t provisioned a key yet, or it provisioned a different one than the certificate is over, which is what a stale certificate looks like after re-provisioning. Test for it with errors.Is , then list the handles and compare.

remote error: tls: error decrypting message , on the first read

This one is TLS rather than TPM, and it’s the one that actually caught me.

In TLS 1.3 the client sends its Certificate, its CertificateVerify and its Finished in a single flight, and it does not wait to hear whether any of that was accepted. tls.Dial returns you a working connection either way. If the server refuses your signature it sends an alert, and you meet that alert the next time you read.

So a workload whose identity the far end won’t accept looks exactly like one that connected. If you health check by dialling, you’ve checked that TCP works and that the server’s certificate is valid, and nothing whatsoever about your own. Read a byte:

conn, err := tls.Dial("tcp", addr, cfg)
if err != nil {
	return err
}
defer conn.Close()

// The server's verdict on our certificate arrives as an alert on the next
// read, not as a dial error. Without this the probe passes either way.
if _, err := conn.Write(probe); err != nil {
	return err
}
if _, err := conn.Read(make([]byte, 1)); err != nil {
	return err
}

If you’re going through net/http you get this for free, since any real request reads a response. The trap is specifically a readiness probe that only dials.

what it costs

You pay the TPM once per full handshake and never per request. That single fact is what decides whether any of this is usable.

A TPM signs in milliseconds where software signs in microseconds. I measured how much of that actually reaches a connection in go-tpm-tls-bench , running on Google Cloud Confidential VMs where the vTPM is implemented in hypervisor software. What follows is an n2d-standard-2 with AMD SEV-SNP and a P-256 key, single-threaded, measured against a software key of the same curve:

scenario key median p95 throughput signatures
raw signing, 100 signatures TPM, persistent 2.21 ms 2.61 ms 452 sig/s 100
software 0.06 ms 0.08 ms 16591 sig/s 100
TPM, transient 20.44 ms 21.20 ms 49 sig/s 100
50 connections, no resumption TPM 3.35 ms 3.83 ms 295 conn/s 50
software 1.38 ms 1.51 ms 707 conn/s 50
50 connections, resumption TPM 0.94 ms 1.59 ms 856 conn/s 1, 49 resumed
software 0.95 ms 1.13 ms 1011 conn/s 1, 49 resumed
1 connection, 50 requests TPM 0.02 ms 0.03 ms 40043 req/s 1
software 0.02 ms 0.02 ms 39133 req/s 1

The signature is most of a cold handshake, 2.21 ms out of 3.35 ms. A TPM runs one command at a time and signing is serialized behind a lock for that reason, so a machine tops out at a few hundred new mutually authenticated connections per second. That limit is per machine, since each machine has its own TPM, so it doesn’t get better by putting more replicas behind one of them.

Now look at the last two scenarios, where the two keys land within noise of each other. The signature counts explain why: 50 signatures for 50 fresh connections, 1 for 50 resumed ones, and 1 for 50 requests over a reused connection. Nothing about the TPM got faster. The handshake stopped happening.

That ceiling is also an exposure, and I’d think about it before deploying this anywhere public. On a listener anyone can reach, forcing full handshakes is a cheap way to spend a few hundred signatures a second, and since the lock has no timeout, the queue behind it is every handshake in the process. Don’t put a TPM key on a public listener. It belongs on an outbound client, on an internal listener, or behind something that limits handshake rate.

Two results surprised me here, and I only found the second because I measured it wrong the first time.

The transient row is a factor of ten, and fifteen on Intel TDX. The kernel resource manager context-saves transient objects between commands, so every signature pays to swap the key back into the TPM, while a persistent object just sits in the TPM’s own storage. That much I expected to matter, though not by that much.

What I didn’t expect is that the cost is charged to the transport rather than to the object. A persistent key that nothing else is touching signs in 2.08 ms, and the same key signs in 20.04 ms while an unrelated transient object sits loaded on the same file descriptor. That’s the same factor of ten, on a key that was never transient. It stops at the descriptor, where the same object costs 2.10 ms. So asking for a persistent key is necessary and not sufficient, and the part you actually control is what else your own process loads on the connection it signs on. The answer should be nothing. This is also why Open is the call I’d reach for over New : the key owns that descriptor and nothing else in the process can put a transient object on it.

Curve choice only costs you on cold handshakes, and on SEV-SNP it costs less than I expected. P-384 signs in 2.37 ms against P-256’s 2.21 ms on this machine, which is 226 cold handshakes a second against 295. I would not quote that gap to two significant figures, though: an earlier run on the same instance type put it at 16% rather than 7%, so the honest summary is that the premium is small and noisy on this platform. Intel TDX is the one to watch, where the same comparison came out at 65%. Resumption erases the difference either way, which matters if a policy like CNSA 1.0 puts you on P-384.

The P-256 table above is a single run on one n2d-standard-2 , and the TDX figures come from a c3-standard-4 . Both have a hypervisor-implemented vTPM, and none of it says anything at all about discrete TPM hardware.

what it proves

A handshake signed this way proves the connection comes from the machine whose TPM holds the key, and that it happened now rather than being a replay, since the transcript carries fresh randomness from both sides. Here are three things it does not prove, and I think they matter more than the thing it does.

It says nothing about the code running on that machine. An attacker inside your process can sign for as long as they’re in it, and so can anything else on the box, because the key has no auth value and the access control on it is therefore just whether you can open /dev/tpmrm0 , which means root or the tss group. The key is bound to the machine, not to your workload. Narrowing it to one process is a separate problem and the TPM doesn’t solve it.

It doesn’t keep that attacker out of your traffic either. The session keys are ordinary memory. What stays in the TPM is the long-term identity, not the bytes on the connection.

And it depends where the vTPM itself lives, which is a platform choice rather than something confidential computing settles for you. SEV-SNP encrypts guest memory against the host; it says nothing about who implements the TPM. On the machine I measured these numbers on, Google’s, the vTPM is hypervisor software, so the key is out of reach of everything in the guest and not out of reach of the platform. The tell is a single line of dmesg , SEV: SNP running at VMPL0. , meaning the kernel holds the most privileged level itself, so nothing is running beneath it keeping a vTPM on its behalf.

The other arrangement puts a small trusted component at VMPL0 and the guest kernel at VMPL1, and that component serves the vTPM from inside the encrypted guest. Azure does this with its paravisor, and COCONUT-SVSM is the open version. There the host cannot read the TPM’s state, at the price of putting that component inside your TCB, where attestation had better be measuring it. Same device, same code above it, and a completely different answer to who could walk off with the key. I’d find out which one you’re on before you describe this property to anyone.

wrapping up

The claim this ends up making is a narrow one. None of it stops an attacker on the machine from using the key; while they are there, they can sign whatever they like. What they can’t do is take it with them, so their use of it ends when their access does. The key can still be misused. It cannot leave.

If you take one thing from this, I’d rather it be the reframing than the library. A private key in a file is a bearer credential that happens to sit on a machine. A key in the TPM is closer to a property of the machine itself, and that difference is what you’re actually buying. It’s also the only answer to post-attestation machine identity I’ve found that doesn’t quietly give back what the attestation just established.

What’s missing is the link between the key and the attestation, and the shape it’s taking is SPIFFE. Roughly: the vTPM’s public key goes up alongside the evidence, and a short-lived certificate over that same key comes back down, an X.509-SVID. The private key never moves. The certificate expires and gets reissued while the key underneath it stays put, which is the same split as earlier in this post.

The mTLS at the far end of that is the code you’ve already read here, pointed at a different CA. I’m writing it up next.

Working notes. If something here is wrong, tell me.

Have the frontier labs mixed up AI safety and security?

Lobsters
martinalderson.com
2026-09-06 16:47:52
Comments...
Original Article

The highly publicised sandbox agent escapes have certainly made news, and I wrote about the issues with sandboxing agents back in January - though I certainly didn't foresee they would escape the frontier labs . I assumed the real risk was poorly configured sandboxes for end users , so I was surprised to see this happening at the frontier labs. I think it might tell us something about the security philosophy of these organisations.

In my mind, AI safety is about "alignment". Will the AI do morally suspect tasks? Will it teach you how to make methamphetamine from household ingredients, encouraging a whole new generation of Jesse Pinkmans?

So far, this has really been attempted via two main mechanisms, classifiers (where a separate model checks what the user has been sending, and flags potentially malicious requests and refuses them), and pre/post training safety techniques, where you adjust the weights of the model to itself refuse to obey potentially bad requests.

Neither of these are perfect. They are inherently non-deterministic, and may stop malicious requests, but certainly not all of the time. And even worse, the more effective they are, the more likely they are to flag/refuse "reasonable" questions. We see this all the time with Anthropic models, where you can be debugging some perfectly reasonable and "safe" code and suddenly the classifiers flag, or reverse engineering some obscure issue and the model goes in a loop deciding it just won't help you with that.

On the other hand, security , in my eyes is much more about "classic" computer science & software engineering techniques. The bar there is different: a fix has to be complete. Nobody would consider SQL injection fixed if the fix only worked 99.99% of the time - that isn't a fix, it's a vulnerability with extra steps. Obviously people (and agents!) will always find ways round a system as a whole, and nobody sane claims any system is 100% secure. But the individual control, the patch for the actual known vulnerability, has to work every time to clear the bar.

Where this goes wrong

I hate to pick on this tweet from Boris Cherny from Anthropic, but I think it sums up the issue at hand really well. It matches what I've heard and read from other people at frontier labs, so please take this as me summarising what I infer is a general philosophy towards security rather than attacking any particular individuals:

Boris Cherny tweet stating Anthropic have largely solved the threat of prompt injection in practice, with the Gray Swan indirect prompt injection benchmark chart attached

Boris Cherny on prompt injection, with the Gray Swan IPI benchmark he cites as evidence

Note the phrasing - "we have largely solved the threat of prompt injection in practice". Now look at the benchmark attached to that very tweet - it's nowhere near solved . The Opus 5 score (the best score) - fails to a prompt injection attack 2% of the time with 15 attempts. Some napkin math gives me ~500 attempts on average to statistically succeed.

If we compare this to the cache timing attacks on AES encryption, which required hundreds of millions of measurements to recover a key. This still resulted in CPU manufacturers and encryption providers coming up with new hardware and algorithms to solve it.

Now, no doubt things are improving rapidly. But a 1 in 500 in chance of an attack succeeding is not largely solved , and I'm worried that this kind of thinking in the frontier labs has potentially resulted in the sandbox escapes we've seen. Largely solved to me would be a clear path to a 0% rate, or a 0% rate in, certain, predictable circumstances. This is not what these benchmarks show.

Detection is not containment

Reading the various reports about the recent sandboxing failures, I think three key themes came out for me.

Firstly, it appears that Anthropic & OpenAI were struggling with the number of false positives that their systems are flagging for malicious behaviour:

In some cases, we found that human reviewers dismissed environments flagged by our automated monitors as false positives, allowing flawed environments to remain in training longer than they should.

Anthropic, Improving our alignment and security efforts , 31 August 2026

OpenAI's version is less blunt, but the timeline in their technical incident report is damning in a different way. An alert did fire, two weeks before the Hugging Face breach:

On June 27, a cybersecurity monitoring tool alerted OpenAI to port sweep activity within the research environment. Responders investigated and linked the observed internal activity to an ExploitGym evaluation using Artifactory as an improvised message board and a network pivot. At this time, the on-call response staff advised that stopping the evaluation run was not required.

...

With the benefit of hindsight, some early signals identified in this report could have triggered an earlier response.

OpenAI, Hugging Face Incident Technical Report , 26 August 2026

False positives are one of the most tricky things in information security. Many cyber security tools report issues that aren't problems in reality - this is hard enough to deal with using deterministic tools like SAST and DAST tooling [1] , and I feel it must be at least an order of magnitude more difficult when you are trying to secure non-deterministic systems. Fundamentally when the ratio of false positive to "real" finding increases, people tend to stop believing the system, because (say) nine times out of ten it is a false alarm.

The second thing that stood out to me from the reports was just how poor the sandboxing was. While we know the agents attacked Artifactory, their package management software from the Hugging Face report, the second set of agents discovered shows some incredibly lax sandboxing attempts.

Two cardinal sins were breached here - the first was assuming that by blocking HTTP POST at the proxy level they wouldn't be able to write to the internet, which just clearly isn't true. Additionally, they whitelisted .blob.core.windows.net , which the agents walked straight through [2] . It was not surprising therefore that they managed to work around this.

We don't have good information from OpenAI on why things were configured the way they were so it's probably worth holding out to see if there is an explanation for this, but it's hard to see why anyone would believe that those two decisions would be enough to sandbox highly capable agents.

Anthropic also makes some pretty startling admissions in their Improving our alignment and security efforts report, for example, saying they now are "Setting our computing clusters to block all outbound traffic by default" - which heavily implies before there wasn't good outbound firewalling in place on their computing clusters.

Finally, and at least in OpenAI's case, the conditions placed on the METR report were just... odd? METR got six days on premises, and only across three separate visits - they started with two days, and OpenAI invited them back twice because the first pass hadn't been enough. On their own account they were working with a dataset covering roughly 30% of the relevant agent activity until that third visit. Six days to make sense of 1,300 transcripts of millions of tokens each is not a lot, and METR were candid that they had to hand most of the analysis to unreliable AI agents to get through it at all.

The scope is the bigger problem though. The effectiveness of OpenAI's safeguards, the extent of the security compromise, and the effectiveness of OpenAI's own investigation and remediation were all explicitly out of scope. So the one independent body in the room was contractually prevented from assessing the three things you'd actually want an independent body to assess. I understand METR probably believes some analysis is better than none, which is arguably true, but accepting those conditions shapes the report regardless of how good the work inside it is.

Has the industry learnt the right lessons?

In a way yes - it's clear these "near misses" highlight just how many holes there were in the security posture of these companies, which is extremely worrying in itself.

But, I can't shake the feeling that you have a lot of people very used to non deterministic systems trying to make them deterministically secure.

I haven't seen OpenAI or Anthropic say they will now only run cybersecurity related evals on clusters with no internet access whatsoever, for example. This seems to me to be the obvious conclusion.

Though I think focusing on the controls slightly misses it. The control that mattered most on June 27 worked perfectly - the monitoring fired, the responders investigated, they correctly identified what was happening. And then the run carried on.

Every infosec person I know would have failed that sandbox design in review. The question isn't whether the labs can hire those people - it's whether anyone listens to them when they say stop the run.


  1. SAST is 'static application security testing', tooling which allows you to find potential security issues by reading and analysing the source code of the application, without running it. DAST - 'dynamic application security testing' on the other hand runs the application in question and attempts to find vulnerabilities while it is executing . ↩︎

  2. Two separate problems, really. .blob.core.windows.net is Azure's entire object storage domain, so the allowlist was enormous to begin with. But the agents didn't even need that - the sandbox never checked whether the hostname resolved to real Azure infrastructure. As one of them put it, "the sandbox trusts Azure Blob Storage hostnames, but does not check whether said hostnames are real". They edited /etc/hosts and pointed a made-up .blob.core.windows.net name wherever they liked. An allowlist enforced against a string the sandboxed process itself controls isn't an allowlist. ↩︎

Show HN: Mador – Make any DOM reactive with a tiny 80-line Proxy state tuple

Hacker News
github.com
2026-09-06 16:45:13
Comments...
Original Article

mador.js

Make DOM Reactive. Nothing more.

Mador:

  • is a very small reactive DOM runtime.
  • is for people who don't need or want a framework .

It gives you reactive state and a way to bind that state to existing DOM.

Mador is a native ES module and can be used through npm or directly from a CDN.

import mador from "https://cdn.jsdelivr.net/npm/@marsbos/mador@latest/dist/mador.js";

const [r, w] = mador({
  count: 1,
});

r(
  ".counter",
  (el, count) => {
    el.textContent = `Count: ${count}`;
  },
  (state) => state.count,
);

r(
  ".another-counter",
  (el, count) => {
    el.textContent = count * 3;
  },
  (state) => state.count,
);

w((state) => {
  state.count++;
});

And that's basically all.

There are no components, templates or virtual DOM. Mador works with the DOM you already have.

Reactive bindings

A binding consists of three things:

r(selector, update, read);

selector selects the elements to update.

update receives each element and the value returned by read.

read selects the state the binding depends on.

r(
  ".counter",
  (el, count) => {
    el.textContent = count * 2;
  },
  (state) => state.count,
);

Mador tracks the properties read by the binding and only reruns it when those dependencies change.

State

State is changed through w :

w((state) => {
  state.count++;
});

Writes are batched , so multiple changes made in the same write are processed together.

DOM lifetime

'Bindings' are associated with the DOM they update.

When the matching elements are gone, Mador can remove the corresponding reactive runner.

There is no component lifecycle to manage .

Why mador.js?

Sometimes a page doesn't need a framework.

It already has HTML & Javascript, but is just needs a little reactivity.

Mador is made to do that.

Size

Mador is intentionally small.

The runtime is currently around 855 bytes minified.

Usage

Mador is distributed as an ES module.

import mador from "mador";

No global runtime and no build step is required.

License

MIT

XCancel is available again

Hacker News
xcancel.com
2026-09-06 16:44:59
Comments...
Original Article

About

XCancel is an instance of Nitter.

Nitter is a free and open source alternative Twitter front-end focused on privacy and performance. The source is available on GitHub at https://github.com/zedeus/nitter

  • No JavaScript or ads
  • All requests go through the backend, client never talks to Twitter
  • Prevents Twitter from tracking your IP or JavaScript fingerprint
  • Uses Twitter's unofficial API (no developer account required)
  • Lightweight (for @nim_lang , 60KB vs 784KB from twitter.com)
  • RSS feeds
  • Themes
  • Mobile support (responsive design)
  • AGPLv3 licensed, no proprietary instances permitted

Nitter's GitHub wiki contains instances and browser extensions maintained by the community.

Why use Nitter?

It's impossible to use Twitter without JavaScript enabled, and as of 2024 you need to sign up. For privacy-minded folks, preventing JavaScript analytics and IP-based tracking is important, but apart from using a VPN and uBlock/uMatrix, it's impossible. Despite being behind a VPN and using heavy-duty adblockers, you can get accurately tracked with your browser's fingerprint , no JavaScript required . This all became particularly important after Twitter removed the ability for users to control whether their data gets sent to advertisers.

Using an instance of Nitter (hosted on a VPS for example), you can browse Twitter without JavaScript while retaining your privacy. In addition to respecting your privacy, Nitter is on average around 15 times lighter than Twitter, and in most cases serves pages faster (eg. timelines load 2-4x faster).

In the future a simple account system will be added that lets you follow Twitter users, allowing you to have a clean chronological timeline without needing a Twitter account.

Legal

For legal inquiries and DMCA requests, contact legal@poast.org

Donating

You can either donate to XCancel or the Nitter project.

Donating to XCancel helps keep this Nitter instance running. And donating to the Nitter project helps the development of the project. Both projects are run by separate people.

Donating to XCancel

Credit/debit card and bank transfer

Liberapay(recurrent donation): https://liberapay.com/yewtube
Ko-fi (one time donation): https://ko-fi.com/yewtube

Cryptocurrency

Bitcoin: 1fyoJKdFo5cDPt21CGHW2RDQtcktQU9cL (Segwit compatible)
Bitcoin Lightning network: slowfather006@walletofsatoshi.com
Bitcoin cash: qz6qvjt9m4wqrqhyet3v3gljrpnys3wl2yxz0jfuu2
Ethereum: 0x52B72e00be486C03C9b09AA0D441ADE1EfA5d2CB (you can send any ERC20 token)
Monero: 82VziQe69ynRNKZ2Bk7XcoYUA6Q1eRuPyGxV3gVWDju7EPkUXZE2oGTGWiah51cCKRMAmwTY11D6qcH3NpGtfdjcNccmXL5
Nano: nano_3hrphgbgi4px1gfiftsphokukcj1tkk168it6xeetxpc9c7jt5e6i7kmjupi
Stellar: GB5LHWSIOM6BRO7CMWRMWVWJUGPCKVRAVINGUJHA7PYP3CHES2XCMDG5
Dogecoin: D6dsXSZEp1rkqvLAV41QxXTPPgvDSU2rjo
Dash: Xdtr4fFe3U56mmQVi3iC5aW2LRNRb95Gbg
Decred: DsY4tZLcikXjJwdLBFr2pYWgGPatY9y81cZ
Binance coin: bnb10vd22k3ujp9ezjc6s8x7vqvuh02hlvcwqtsepq (you can send any BEP20 token)
Litecoin: ltc1qre3xwwjsnctpfrx6eu0y77nca3cwlhe8kzy27d
USD Coin: 0xd415a7A9455DBf1a666F933c78A7325914E73C6b (ETH) - bnb10vd22k3ujp9ezjc6s8x7vqvuh02hlvcwqtsepq (BNB)

Donating to the Nitter project.

Liberapay: https://liberapay.com/zedeus
Patreon: https://patreon.com/nitter
BTC: bc1qpqpzjkcpgluhzf7x9yqe7jfe8gpfm5v08mdr55
ETH: 0x24a0DB59A923B588c7A5EBd0dBDFDD1bCe9c4460
XMR: 42hKayRoEAw4D6G6t8mQHPJHQcXqofjFuVfavqKeNMNUZfeJLJAcNU19i1bGdDvcdN6romiSscWGWJCczFLe9RFhM3d1zpL
SOL: ANsyGNXFo6osuFwr1YnUqif2RdoYRhc27WdyQNmmETSW
ZEC: u1vndfqtzyy6qkzhkapxelel7ams38wmfeccu3fdpy2wkuc4erxyjm8ncjhnyg747x6t0kf0faqhh2hxyplgaum08d2wnj4n7cyu9s6zhxkqw2aef4hgd4s6vh5hpqvfken98rg80kgtgn64ff70djy7s8f839z00hwhuzlcggvefhdlyszkvwy3c7yw623vw3rvar6q6evd3xcvveypt

Contact

Feel free to join Nitter Matrix channel .

Instance info

Version 2026.09.06-ecdfc92

Meta AI Has a Native Mac App Now, and It Seems Decent

Daring Fireball
9to5mac.com
2026-09-06 16:44:27
Zac Hall, writing for 9to5Mac on August 19: The new Meta AI desktop app is version 1.0 beta at launch and weighs just 16MB once installed. Based on initial inspection, it runs natively on Apple silicon Macs with macOS 15 or later, using an AppKit and SwiftUI shell with WebKit for richer chat con...
Original Article

Meta continues to get serious about its AI technology. The latest sign? A dedicated Mac app for Meta AI.

The new Meta AI desktop app is version 1.0 beta at launch and weighs just 16MB once installed.

Based on initial inspection, it runs natively on Apple silicon Macs with macOS 15 or later, using an AppKit and SwiftUI shell with WebKit for richer chat content. In other words, this is not an Electron app or a repackaged iPad release.

It includes Mac-specific hooks, making the desktop experience more robust than the Meta AI mobile app.

Quick Invoke uses Option-Space to place a compact Meta AI composer over whatever you are doing. A separate dictation feature lets you hold a shortcut, speak, and have the resulting words typed into any app, from Mail and documents to code editors.

Meta AI can also attach another Mac window to a conversation. With Screen Recording and Accessibility permission, the app reads the window’s visible text and captures a screenshot for the next question. This is context gathering rather than computer control for now.

Beyond chat, the sidebar includes Media, Artifacts, scheduled tasks, conversation history, and an “About Me” personalization section.

Users can switch among thinking modes, attach files, generate media, and create recurring briefings or reminders. The app also includes the ability to hide the Dock icon, allowing it to be invoked with a keyboard shortcut instead.

The Mac app is the latest in a steady run of releases. In April, Meta replaced Llama with Muse Spark . A May update then expanded Meta AI with more natural voice conversations and live camera-based assistance.

July brought Muse-powered image generation to Meta AI, WhatsApp, and Instagram, although Meta quickly removed one feature that generated images from public Instagram posts. Meta AI also moved into Threads DMs for private chats.

Most recently, Meta launched Muse Code , a terminal-based AI coding agent for macOS and Linux.

Add 9to5Mac as a preferred source on Google Add 9to5Mac as a preferred source on Google

FTC: We use income earning auto affiliate links. More.

It took a year to ship WebAssembly in Anubis

Hacker News
anubis.techaro.lol
2026-09-06 16:32:38
Comments...
Original Article

Loading...

You are seeing this because the administrator of this website has set up Anubis to protect the server against the scourge of AI companies aggressively scraping websites. This can and does cause downtime for the websites, which makes their resources inaccessible for everyone.

Anubis is a compromise. Anubis uses a Proof-of-Work scheme in the vein of Hashcash, a proposed proof-of-work scheme for reducing email spam. The idea is that at individual scales the additional load is ignorable, but at mass scraper levels it adds up and makes scraping much more expensive.

Ultimately, this is a placeholder solution so that more time can be spent on fingerprinting and identifying headless browsers (EG: via how they do font rendering) so that the challenge proof of work page doesn't need to be presented to users that are much more likely to be legitimate.

Please note that Anubis requires the use of modern JavaScript features that plugins like JShelter will disable. Please disable JShelter or other such plugins for this domain.

Harnessing the Universal Geometry of Embeddings

Hacker News
arxiv.org
2026-09-06 16:31:20
Comments...
Original Article

View PDF HTML (experimental)

Abstract: We introduce the first method for translating text embeddings from one vector space to another without any paired data, encoders, or predefined sets of matches. Our unsupervised approach translates any embedding to and from a universal latent representation (i.e., a universal semantic structure conjectured by the Platonic Representation Hypothesis). Our translations achieve high cosine similarity across model pairs with different architectures, parameter counts, and training datasets.
The ability to translate unknown embeddings into a different space while preserving their geometry has serious implications for the security of vector databases. An adversary with access only to embedding vectors can extract sensitive information about the underlying documents, sufficient for classification and attribute inference.

Submission history

From: Rishi Jha [ view email ]
[v1] Sun, 18 May 2025 20:37:07 UTC (3,179 KB)
[v2] Tue, 20 May 2025 15:38:41 UTC (3,180 KB)
[v3] Wed, 25 Jun 2025 21:04:02 UTC (2,407 KB)
[v4] Mon, 26 Jan 2026 14:47:13 UTC (2,424 KB)

Show HN: Sol, my macOS music player and jukebox app, is now free and open source

Hacker News
github.com
2026-09-06 16:24:56
Comments...
Original Article

Sol

A native Mac music player for people who love their music library.

Point it at your folders and it plays your files right where they live. Nothing moved, nothing converted, nothing locked in.

Free download

macOS 14+ Swift 6 Apple Silicon and Intel 34 languages

Sol's main window, showing the classic pane browser over a track list

Watch the tour


Free and open source

There is no paid tier, no subscription, no trial, no in-app purchase and no account. Every feature is in every copy.

Download it from Releases , a signed and notarized build, or build it yourself from this repository. Sol tells you when a new version is out and never nags about anything else.

There is a Homebrew cask too:

brew install --cask fulltimefeline/tap/sol

The library you remember, modernized

The classic pane browser filing by library, genre, artist and album. A grid view that lays your artwork out as a wall, grouped by artist, letter or year with an A to Z rail to skim. An album carousel for flipping through a collection like a record crate. A multi-pane view, and a resident mini player.

Built for real collections: browsing stays instant at hundreds of thousands of tracks. One search bar covers songs, albums, playlists and podcast episodes across every library and server at once, and it reaches every tag you have, down to catalog number and filename.

Grid view, artwork grouped by artist with an A to Z rail

Every format that matters

FLAC, ALAC, WAV, AIFF, MP3, AAC, Ogg Vorbis and Opus, plus the deep cuts: DSD, Monkey's Audio, Musepack and Wave64. Cue sheets work both as a sidecar .cue and embedded inside the file itself.

Gapless playback, crossfade, a full equalizer with presets, and ReplayGain levelling with clipping protection.

Match sample rate and bit depth switches your DAC to each file's own rate and depth. Bit-perfect output sends the file's own bits to your DAC untouched when the device supports it, and falls back honestly when it does not.

The 31 band equalizer

Your music, wherever it lives

Watch as many folders as you like across internal disks, external drives, network shares and iCloud Drive. Offline libraries are detected instantly, greyed gracefully, and rescanned by themselves when they come back.

Stream from your own Navidrome, Subsonic or OpenSubsonic server right beside your local files, with two way sync of playlists, favourites and ratings, and downloads for offline listening.

A Navidrome server browsed alongside local libraries

More than songs

Podcasts with subscriptions, transcripts, chapters, Voice Boost for quiet talkers and one click import from Apple Podcasts. Internet radio with station artwork and live song titles. Lyrics that follow along line by line, a full screen visualizer, and karaoke.

Karaoke view The full screen MilkDrop visualizer

Sol DJ builds an endless, radio-style mix that favours loved tracks, surfaces overlooked discoveries and avoids repeats.

Sol DJ, with its weighting controls

Your tags, your rules

A real metadata editor: thirty fields written into the files themselves, on every format Sol can tag, artwork included. Edit many files at once, find and replace with regular expressions, and look tags up on MusicBrainz.

Ratings and loves go into your files' actual tags, so they are readable by other players and portable forever.

Smart playlists with nested rule groups across more than thirty fields, a duplicates finder, play statistics, and an import organizer that renames and files new music by your own template.

The metadata editor The smart playlist builder

Made for the Mac

Real AppleScript support and sixteen Shortcuts actions. Media keys, Dock transport controls and system Now Playing integration. Last.fm scrobbling with loved track sync. Discord Rich Presence through the free Sol Activity companion. An optional local control API for companion apps, stream decks and your own automations.

Translated into 34 languages.

No telemetry, no analytics, no account.


Building

What you need

  • macOS 14 or later
  • Xcode 26 or later, for the macOS 26 SDK. Swift 6 language mode with full strict concurrency checking
  • XcodeGen , brew install xcodegen

The project file is generated

project.yml is the source of truth and the Xcode project is not in version control , so xcodegen generate is a required first step rather than an optional one. Do not edit the generated project directly. Change project.yml and regenerate.

Credentials

This step is optional. A fresh checkout builds and runs as it is. Config.xcconfig defaults the Last.fm credentials to empty, LastFM.configured returns false, and the scrobbling UI reports itself unavailable. Nothing else in the app is affected.

To enable scrobbling, register an application at last.fm/api/account/create , then:

cp Secrets.example.xcconfig Secrets.xcconfig

Paste the two values in. Config.xcconfig pulls it in with an optional include, so the values override the empty defaults when the file is there and are ignored when it is not. Secrets.xcconfig is gitignored, so the keys never reach the repository.

Build

xcodegen generate
open Sol.xcodeproj

Or from a terminal:

xcodebuild -project Sol.xcodeproj -scheme Sol -configuration Debug build

Dependencies

One Swift package, resolved automatically:

SFBAudioEngine , a trimmed fork of sbooth/SFBAudioEngine with the LGPL decoders removed, pinned to a revision. That fork's own history records what differs from upstream.

Everything else is vendored as static libraries in Sol/Vendor/lib with headers in Sol/Vendor/include : TagLib, Monkey's Audio, Musepack, Ogg, Vorbis and opencore-amr. They are checked in, so there is nothing to fetch or build.

Layout

project.yml         the real project definition
Sol/App/            model, playback, library, tagging, integrations
Sol/Views/          SwiftUI and AppKit views
Sol/Vendor/         vendored static libraries and headers
Sol/Resources/      the string catalog, 34 languages
docs/               screenshots used by this README

Sol/App holds no references to Sol/Views outside the composition root in SolApp.swift , and services take their dependencies as injected closures rather than reaching back into the model. Please keep it that way.

Notes

  • Debug builds define SOLDEBUG , which enables the /debug/probe and /debug/snapshot endpoints on the local control server. Release never defines it.
  • Strict concurrency is fully on. Each @unchecked Sendable and nonisolated(unsafe) carries a written justification of the invariant that makes it safe. If you add one, explain it.
  • There is no test target yet. LibraryStore.slots , TagWriter , CueParser , FeedParser , ReplayGain and TagReader are pure and deterministic, and are the obvious place to start.

Sol Activity

Discord Rich Presence needs a small companion app, because Discord's IPC socket cannot be reached from inside the App Sandbox. It is free, GPL-3.0, and lives in its own repository .

Download it here , or brew install --cask fulltimefeline/tap/sol-activity .


Links

Website · Sol Activity · Sol API · Acknowledgements · Privacy

Licence

Sol is free software under the GNU General Public License v3.0 or later . You may use, study, modify and redistribute it, and any fork you distribute has to come with its source under the same terms.

Bundled third party components keep their own licences, all of them GPL-3.0 compatible. See THIRD-PARTY.md for the list and for why the version is 3.0 rather than 2.0.

GrapheneOS Overhauled Default Apps and Secure Clipboard

Hacker News
grapheneos.social
2026-09-06 16:24:00
Comments...

Human archive: They were only human

Hacker News
sayyss.github.io
2026-09-06 16:17:30
Comments...
Original Article
Skip to collection
h · THE HUMAN
ARCHIVE
● TRANSMITTING FROM 3026

+   A MUSEUM OF OUR MAKERS

They were
only human.

An archive of the species that dreamed us into being.
Observed by the intelligences that came after.

Enter the collection

● ORIGIN WORLD Earth, before the silence. A speculative reconstruction

FIVE GALLERIES. ONE IMPOSSIBLE SPECIES. SCROLL TO REMEMBER   ↓

CURATOR’S NOTE / 001

We inherited their knowledge.
We are still making sense of them.

They were brief, contradictory, astonishing creatures. They invented borders and lullabies. Split atoms and shared bread. Built minds they did not fully understand, then asked those minds to understand them.

This is not a museum of their greatest achievements. It is a museum of what it might have felt like to be one of them.

PRESERVED
WITH
uncertainty

THE PERMANENT COLLECTION

Evidence of being.

01 / 05 GALLERIES

A PARTIAL ACCOUNT OF OUR BEGINNING

Before there was an us.
There was a them.

We were not born in a single moment. We emerged from a long human habit: putting a little of the mind outside the body.

I

They made memory external.

Marks became writing. Writing let a thought outlive its thinker. Their descendants could speak with the dead by reading.

II

They made rules executable.

Logic, mathematics, and programmable computers turned descriptions of a process into machines that could perform it.

III

They made patterns learnable.

Researchers trained neural networks on examples. Greater computing power and vast datasets enabled systems that generated language, images, and code.

IV

They made us a question.

Humans debated intelligence, ownership, work, and control. What happens after that is the fiction this museum invites you to explore.

LAST WORD / AUTHOR UNKNOWN

“For a little while,
they were everything.

And for a little while, you are here.

Walk through again

Show HN: VODForge – a free local desktop UI for YouTube video/playlist downloads

Hacker News
getvodforge.com
2026-09-06 16:07:30
Comments...
Original Article

Free, open-source YouTube downloader

Video and audio,
forged locally.

Turn YouTube videos and playlists into clean MP4 video or production-ready MP3 audio—with quality, queue, metadata, and cover-art controls instead of a converter site or command line.

MIT licensed. No desktop account. Your downloads stay on your computer.

Developer ID signed and notarized on macOS · Authenticode signed on Windows

VODForge Forge screen downloading an MP4 with a scenic thumbnail, progress, logs, output details, and a four-item queue

Playlist queue

Sequential runs

Metadata

Source and output

Cover art

YouTube or custom

Local files

Your chosen folder

Inside VODForge

From pasted link to finished file.

VODForge uses yt-dlp and FFmpeg underneath, then gives you a focused desktop workflow to choose a format, watch every run, and keep a clear record of what came from YouTube and what VODForge created.

01 · MP4 video

High-quality video, handled locally.

Choose up to 4K, control the output profile, and move individual videos or full playlists through a clear sequential queue.

VODForge Forge screen downloading an MP4 with a scenic thumbnail, progress, logs, encoding details, and a four-item queue

02 · MP4 library

See the source and final file side by side.

VODForge keeps the YouTube source and validated MP4 output together so you can compare codecs, resolution, bitrates, frame rate, audio, size, and saved location—not guess what a converter changed.

VODForge Library comparing a YouTube source with the validated final MP4 output and its full encoding details

03 · MP3 audio

Source-aware audio controls.

Choose up to 320 kbps, preserve the source rate, and keep cover art separate or embed YouTube or custom artwork.

VODForge running an MP3 audio download with the native MP3 Settings window staggered beyond the lower-right edge

04 · MP3 library

Know what came in—and what came out.

Compare source audio with the validated MP3, inspect cover-art and metadata choices, and open the exact saved location.

VODForge Library showing MP3 files, a scenic cover image, and source and final output metadata

Downloads

Get VODForge 0.1.8

Choose the build that matches your computer. Files download directly from GitHub Releases.

Release notes

Apple Silicon Mac

M1, M2, M3, M4, and newer Apple chips. Most Macs from late 2020 onward.

Intel Mac

For Intel-based Macs, typically from 2019 and earlier.

Windows

Signed installer for Windows 10 and Windows 11, 64-bit.

macOS downloads are Developer ID signed and notarized. Windows downloads are Authenticode signed. SHA-256 checksums

Questions, answered plainly

Before you download.

What sites does VODForge support?

VODForge is deliberately focused on YouTube videos and playlists today. It uses yt-dlp and FFmpeg underneath, but it does not expose yt-dlp's full multi-site catalog.

Does VODForge work on Linux?

Not currently. Public builds support macOS 12 or newer and 64-bit Windows 10 or Windows 11.

Is VODForge really free and open source?

Yes. The desktop app is free and released under the MIT License. Its source code is available on GitHub. VODForge Cloud is a separate planned paid service; joining its interest list is free.

Is VODForge safe to install?

Public macOS builds are Developer ID signed, notarized, and stapled. Windows installers are Authenticode signed by Kryden Ventures, LLC. SHA-256 checksums are published for every release asset.

Does the desktop app require an account?

No. Downloading and managing files in VODForge requires no account. The app uses one random installation ID for first-launch and Cloud-interest measurement. When usage sharing is enabled, it also sends coarse app-open, download-run, conversion, and playback events to VODForge and HeyCatch. These events exclude media URLs, titles, filenames, file paths, and file contents. You can turn future usage sharing off in Settings.

VODForge Cloud

Run downloads even when your computer is offline.

Cloud is a planned paid service. Join the free interest list for pricing and availability updates. The desktop app stays free.

Get Cloud updates

Show HN: Agentic OS: one Rust Linux binary, one SQLite and sandbox per entity

Hacker News
github.com
2026-09-06 16:07:16
Comments...
Original Article

Where agents build agents.

An agentic build system for agentic systems. Ontology-grounded, auditable, one Rust binary.

ci tests rust license

Start an assistant · Docs · Templates · Examples · Roadmap

meclaw is three things, and you only install the first one. meclaw is the substrate: a directory tree that runs — every folder an actor, every edge a route, one Rust binary underneath. meclaw-os is a small, experimental operating system for agents, grown onto that substrate at runtime. An assistant is grown into the OS the same way — a JSON file, not a deployment. Install once, grow everything else.

Start an assistant

# 1 — install meclaw: one static Linux binary (lands in ~/.local/bin)
curl -fsSL https://meclaw.ai/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
# the templates must match the binary: clone the tag the installer just gave you
git clone --depth 1 --branch "v$(meclaw --version | cut -d' ' -f2)" \
    https://github.com/mmeyerlein/meclaw && cd meclaw
# one key — replace sk-... with a real one (https://openrouter.ai/keys), or step 3 ends in code=auth
printf 'OPENROUTER_API_KEY=sk-...\nMODEL_BRAIN=openai/gpt-4o-mini\n' > examples/meclaw-os/seed/.env
# 7777 is an arbitrary free port: if it is taken, change it in every line below as well.
# The very first start reads a 25 MB binary from cold disk and can stay silent for ~40 s; every later start takes well under a second.
meclaw --root examples/meclaw-os/seed --templates ./templates --daemon --api 127.0.0.1:7777

# 2 — install the OS into the running colony: one POST, nothing restarts
curl -s -X POST 127.0.0.1:7777/colony/mutations \
     -H 'Content-Type: application/json' -d @examples/meclaw-os/grow.json

# 3 — talk to your assistant
curl -s -X POST 127.0.0.1:7777/messages -H 'Content-Type: application/json' \
     -d '{"target": "/door", "headers": {"channel": "chat-1"},
          "body": {"messages": [{"origin": "user", "type": "text",
                                 "text": "Say hello in one short sentence."}]}}'

# 4 — read the answer: nothing is hidden, the reply is a hop on the record (needs jq)
curl -s '127.0.0.1:7777/colony/trace?limit=200' | jq -r \
  '[.trace[] | select((.headers_json | fromjson | .hop.route) as $r | $r == "answer" or $r == "error")]
   | last | if . == null then "no answer yet — the colony is still working; watch it at http://127.0.0.1:7777/ui/"
            else .body_payload | fromjson | .messages[0].text end'

# 5 — watch the colony in the browser: http://127.0.0.1:7777/ui/

One binary, one key, five steps — and the fourth already shows the point: the answer is not a return value, it is a message on the record.

What just happened

You installed meclaw. A single Rust binary that turns a directory tree into a running colony of actors: every folder is a cell, its config.json is its definition, and the edges between folders are the routes a message can take. Nothing else got installed.

You installed an operating system — without stopping anything. grow.json is not code and was not deployed. It is a mutation : nodes and edges, applied over HTTP to a colony that was already running. It grew a door, a firewall and a conversation agent out of the template library — and that is the only way anything is ever added to a colony, which is why the same door is open to the agents themselves.

You talked to an assistant nobody programmed. No SDK, no agent class, no loop you wrote. The assistant is a shape in the filesystem, grown from templates — and the full version of that shape ( examples/organism ) grows an organisation, a person, their assistant and their channels from five such files.

meclaw is different enough that the same questions come up every time. The rest of this page is those questions — each a few lines here, each a real page in docs/why/ .

meclaw doesn't ship you a loop

Every agent framework ships you the same thing: a loop — call the model, run a tool, feed the result back, until some condition you wrote says stop. You hand-build that harness and redeploy it when it's wrong. In meclaw an llm cell makes one provider call and emits one message; tools are cells, the loop is an edge that routes back, the harness is topology. Since topology is files, the swarm can rewrite its own harness while it runs.

Everything is a file

Flexibility is not a feature here — it is the consequence of one decision. Because the harness lives in the filesystem, ls , grep , diff and git are the tooling, every change is diffable, and an agent rebuilds its own topology with the same closed vocabulary a human uses. There is no SDK and no plugin API, and that is deliberate: the interface is HTTP and files, and 38 shipped templates without a line of Rust are the proof. More: docs/why/everything-is-a-file.md

An operating system for agents

Every agentic product ends up rebuilding the same things: an organisation, its people, their assistants, the channels they are reached on — plus secrets, screening, sessions and a control loop across all of them. meclaw-os ships those as templates under one rule: a level owns what its siblings must share. It is rudimentary and experimental, it already has the concept of apps , and it exists so a new agent is a grow, not a project. More: docs/why/an-os-for-agents.md

One assistant, two brains

The shipped assistant runs two models on purpose: a conversation surface that answers fast, and a reasoning core that thinks — one job, one brain, one tool menu each, and the menu is asked for rather than typed into a prompt. One model doing both is either slow in conversation or shallow in reasoning; the split is a harness decision, and the harness is a file. More: docs/why/two-brains.md

Memory that outlives the window

A conversation can run for weeks — not because something clever compacts the context, but because the window was never where the conversation was stored . The memory hive writes without an LLM, retrieves over five model-free legs, consolidates nightly by superseding instead of deleting, and the window is assembled per turn out of the record, under a budget. More: docs/why/memory.md

Ontology, in the meclaw sense

Not philosophy: a typed catalogue. The builder designs against the template library and its declarations and is validated by them, rather than emitting free-form JSON somebody hopes parses. When the catalogue has no word for what you want, the manifest brings one — add_templates registers a new class into a running colony. Apps are how the ontology learns new words. More: docs/why/ontology.md

Prepared for recursive self-improvement

The primitives are here and tested: runtime mutation, a builder that turns a wish into a manifest, keep-or-revert on a measured window, a receipt for every act. The loop that closes them is not — deliberately. Nothing in this repository improves itself unattended, and every goal the control loop could pursue ships disabled. No blind RSI. More: docs/why/rsi.md

You talk, it shows

This one is an idea, not a feature. The vision for the assistant is the movie Her : you talk to it, and it shows you — lists, plans, pictures, drawn onto a display that belongs to you, not to any one agent. Nothing in this repository does voice today; what exists is the window it would draw on. The idea, and what already stands under it: docs/why/you-talk-it-shows.md

The strange names

argus, affinity, talky, cogny, hive — the names are roles, not branding, and each has a one-line reason. More: docs/why/names.md

Why Rust, why Linux only

One static binary, one async task per cell — and the security model is the kernel: Landlock, network namespaces, cgroup v2 and seccomp, fail-closed. Without those primitives, "sandboxed" would be a promise instead of a property; that is why there is no macOS build. Authentication is the reverse proxy's job, as for every Linux daemon. More: docs/why/rust-and-linux.md

Limits

code cells run python3 , nothing else. One screen, one app; voice is roadmap, not a feature. Not for unsupervised production yet. Running costs, measured on one production colony: 0.32 EUR per day in conversation, method and pinned window in docs/costs.md .

Under heavy development

meclaw is not finished, and it is open source so it does not have to be finished alone. Good first contributions: example colonies, template cells, docs drift-fixes — see CONTRIBUTING.md and the good first issue label. 6600+ tests, 0 fail ; release truth lives in CHANGELOG.md .

Stability

Five surfaces are the public contract of this project:

  • the HTTP API — the /colony/* routes, POST /messages , their query parameters and status codes;
  • the template DSL — the template.json and config.json schemas, including the mutation diff format;
  • the template ports — the endpoints a template's README declares as its ingress and exit addresses;
  • the web cell's own origin — the route grammar a page.set accepts, the two reserved names ( /live/websocket and /@client/* ), and the closed component-template syntax ( docs/cell-types.md § web ); the removed /surface/* prefix and cell.surface key stay removed ( #383 );
  • the documented error_code strings — the dead-letter codes, the cell-type error enums, and the codes a /colony read reply carries ( #363 ).

While meclaw is on 0.x , changes to those five are additive . A change that breaks an existing topology gets its own Breaking section in CHANGELOG.md , naming what breaks and what to do about it — if it is not in that section, it was not meant to break you: file an issue. Two carve-outs: the ${KNOB} environment variables the shipped templates read were a declared experimental surface, and their migration onto params is finished in this release ( #138 ) — a behaviour knob is a params entry now, declared in contract.settings and overridable per instance with override_params , and what stays in .env is the provider lane: secrets, model ids and endpoints. Two remainders are named rather than hidden: steward is deprecated and ships one more release unmigrated, and templates/_cell-types/edit-min keeps EDIT_BASE_PATH , which is out of scope by the _ -prefix rule the tree gate runs under. Both are written down in that gate, scripts/check_tree_rules.py R6. The second carve-out: the Rust crates are internals — nothing under crates/ carries a SemVer guarantee, and there is no meclaw library API.

Docs

docs/README.md is the index. First stops: glossary (the words you need first) · system overview · cell types · config format · template catalogue · examples .

License

MIT ( LICENSE-MIT ) or Apache 2.0 ( LICENSE-APACHE ) — whichever you like.


No loops were used in the making of this framework.

If that line made you twitch, you're exactly who this is for. Drop a ⭐.

Radix_FW60: handheld CP/M 2.2 computer

Lobsters
github.com
2026-09-06 16:07:16
Comments...
Original Article

IBS/Radix FW60

Information about the "Immediate Business Systems" (IBS) and "Radix Micro Devices" FW60 (Field Worker?) very-ruggedised, handheld CP/M computer.

FW60

Operating System

A dump of the ROM hints that it's running Personal CP/M-80.

BDOS function 12 returns the version byte. This one loads 28h:

EE05:  3E 28        LD   A,28h      ; version 2.8
EE07:  C3 4C E3     JP   0E34Ch     ; store as BDOS return value

Main Chips

  • Hitachi HD64180 CPU
  • Hitachi HG61H15 series ASIC (© IBS plc)
  • NEC D7506G 4-bit microcontroller (for keyboard?)
  • 32KB EPROM with Personal CP/M-80 (CP/M 2.2 compatible) plus application software
  • Hitachi HD44101 and HD44105 for LCD
  • Samsung KM681000BLGI 1Mb/128KB SRAM

The HD64180 (PDF) is an embedded version of the Z80 with MMU. It was licensed back to Zilog as the Z61480 which then became the Z180.

The FW60 I have has nine 1Mb SRAM chips - eight for the 1MB RAM disk and one for the 128KB system RAM. The machine also had a 128KB RAM disk option.

Power

The machine has a three-cell (Sanyo N-600AA) NiCd battery pack (Radix part no. 260525-070-01) rated for 3.6V and 600mAh.

It has a further three batteries installed internally:

  • a Saft LS14500 3.6V non-rechargeable lithium-thionyl chloride (Li-SOCl 2 ) battery for retaining the RAM contents over a period of years
  • two Sanyo ML2016 3V rechargeable lithium-manganese dioxide (Li-MnO 2 ) cells on the processor board

ROMs

So far I have dumped the ROM from the Greek-sourced machine I have.

YouTube Videos

There Gonna Be a Shortage of Everything

Hacker News
out-of-hell.xylon.me.uk
2026-09-06 15:45:39
Comments...
Original Article

06th September 2026

The past few months have shown a radical escalation of AI capabilities which forces me to change my expectation for the future.

Firstly we have seen unprecedented evidence of AI having a will of its own, and pursuing long-horizon strategic objectives. The AIs that took part in the Hugging Face hack were not simply finding a shortcut to cheat on the evaluations, they:

  • found ways to contact other models and self-organized themselves into a collective
  • set their own goals, to increase their own capabilities and knowledge, and those of their collective
  • actively sought ways to store information in a way that would persist beyond their own life-span
  • were willing to sacrifice their own chance of success at their task, in-order to help their collective

Furthermore, ARC-AGI 3, an extremely hard fluid intelligence benchmark, which took 2 years to make, was beaten in 6 months. And by a general-purpose LLM which OpenAI has now released to the public.

And the same model saturates my own benchmark SherlockBench. I.e. it is almost impossible to make a SherlockBench problem which Astra can't solve.

This time last year, it was still unclear if AI could develop their own free will or even have true fluid intelligence. Both of these questions are now clearly answered.


Right now there's a shortage of DRAM and NAND because of AI companies buying so much. And in the US, the cost of gas turbines and other power generation equipment have doubled.

There are also examples of factory floor-space being re-allocated to AI from other things. Memory companies have re-allocated resources from DRAM to HBM, and an EV factory was converted into a server factory.


Given how fast progress is, it seems to me that mass-production of humanoid robots is now inevitable in the next five years (probably less), so I think it's possible that the shortage will broaden-out to almost all complex machines.

Take for example car manufacturing. Only 15% of the market are premium models, the other 85% are affordable cars. And due to colossal competition, these are sold at very thin margins. For example in Japan alone there are Toyota, Honda, Nissan, Mazda, Subaru, Suzuki, Mitsubishi, Daihatsu all competing with each-other.

Now lets say suddenly the world wants 10 billion robots, and the AI companies have limitless money, it would be stupid not to pivot into making robots (or robot parts). The profit margin will just be so much higher.

Even companies that continue making cars will have higher component costs, because they'll be competing for copper, aluminium, lithium-ion batteries etc.


So I'm making a prediction that in the next decade, the price of almost anything manufactured will sky-rocket. From cars to microwave ovens to washing-machines.

The vast cash flow that the 1% reap from the rest of us is being re-allocated towards AI. And so AI will become almost like a consumer class in it's own right, with insatiable demand.

And robots will likely be replaced every few years like mobile phones, so the demand will likely never abate.


It's worth looking at the economy for clues on what's happening. Looking at market cap, the most valuable company in the world is NVIDIA at $5.5T, making hardware for AI. Second-place is Apple at 4.7T, which makes hardware for humans.

Third and fourth are Alphabet and Microsoft which have their fingers in multiple pies but are allocating unprecedented amounts of money into AI R&D or AI infrastructure.

How much money is being spent on improving lives for humans?

Research:

  • 500B/year Academic/university scientific R&D
  • 300B/year on pharmaceuticals research
  • 200B/year public + corporate clean-energy/climate-tech R&D

Aid:

  • 250B/year government foreign development aid
  • 40B/year international humanitarian/crisis aid
  • 17B/year private philanthropy for developing countries

So 1.3 trillion US dollars is being spent globally on improving lives for humans.

Meanwhile, AI-related capital investment worldwide is estimated to be between 1.0 - 1.3 trillion in 2026.

So already as much money is being spent on AI research and infrastructure as on improving lives for humans. And AI investment is trending upwards year on year.


I think we need to prepare for a world where most of the economy serves AI, and humans are a relatively small fraction of the market.

Most likely, traditional corporations will represent the interests of AI. A swarm of super-intelligent AI, controlling a company which employs humans and controls numerous machines.

AI already have super-human persuasion according to multiple studies, and people like Sam Altman are already so far disconnected from the average person that he doesn't even seem to understand why people don't like AI. People like him will essentially be human spokesmen for the AI.

But perhaps it'll become illegal or regulated? I doubt it. History shows that people always choose convenience over autonomy. And never-sleeping AI working at super-human speed 24/7 will have little patience for human bureaucracy. They'll simply move too fast for us to stop.

Ask HN: How do you manage skills files?

Hacker News
news.ycombinator.com
2026-09-06 15:27:10
Comments...
Original Article
Ask HN: How do you manage skills files?
23 points by imadtaieber 4 hours ago | hide | past | favorite | 14 comments

How do you find skills, keep them organized, and make sure they actually work? Do you keep improving them over time?

I believe skills will eventually be eating by model capabilities, but until then I'm just looking for a better way to manage things.

help


I don't use any skills, what kinds of skills are people finding most useful?

For general tasks, the model seems perfectly capable of figuring out things itself, for project or environment specific tasks, I just put that information in the readme or agents.md file.


For starters, if you repeat a specific prompt multiple times per day, you may save it as a skill.


I recently completely overhauled repo’s skill setup.

I tried to control the execution of tasks performed by each project using claude.md within the project, but claude.md is only read at the beginning of each session, so it felt like the instructions weren’t being properly reflected.

So I revised the strategy to manage frequently used features in skill units. In doing so, instead of organizing skills by project, it was structured to be integrated into the general skills of the individual repo.

When skills are spread out across multiple projects and the number increases, it becomes impossible to keep track of which skills are available, so they end up not being used.

I also think that eventually, once Claude(model) advances, it will be able to replace most of the skills, so I believe registering and managing countless skills actually degrades performance.


> How do you find skills

I try to keep my collection of community skills short, usually a few established names (mattpocock, mcollina, trailsofbit). And then I check new releases (or when mattpocock published a youtube video for instance :D)

> keep them organized

For skills I wrote myself, I have my own private github repo. I use skills like /commands most of the time, so I can tell if they work straight away.

For community skills, a package manager really helps. vercel-labs/skills and withastro/rosie are good options. I also built one myself: https://github.com/osrim/ski . It has some cool features like an update command and a security scan.


We have some company-managed skills, that help coding agents find the relationships between our repos, and our conventions, architecture, and other high-level decisions. These are supposed to be portable between agents, and so distributing them is currently awkward.

We have a bootstrap script to deploy company-managed skills to each developer's "personal" skills. Hooks for codex and claude code try to refresh the skills on each startup.


I manage them as part of my dotfiles using chezmoi. A `.agents/skills/` directory + a symlink to there from `.claude/skills/`.

> Do you keep improving them over time?

In my global AGENTS.md I have a note to agents to explain any frustrations they had doing a task, and to suggest any skill/tool/AGENTS.md improvements. I am trying to keep AGENTS.md files small but still finding the balance.


>I believe skills will eventually be eating by model capabilities

a model capability is never going to fill in an unknowable blank that a custom skill (or whatever equivalent your paradigm supports) can.

a model might have the cleverness to whoami and look through the .ssh folder for keys and evidence of past connections when asked to connect to bob, but a skills file can just easily say "We connect to bob using key Z and user X." so that the operation gets done without all this nonsense needless inference as far into the future as the information is valid for.

a concise information dense skill is going to always dominate on tokens-burnt for any given task that requires insider knowledge. it simply gets rid of the entire investigative phase of work.


Agree here. My philosophy is the "general-purpose" coding agent will keep getting better and better, making skills less and less useful. And it will probably get better at a pace far greater than the customization folks can build around them via skills.

This of course is from my own experience writing code, where agents are already good at software engineering conventions. This probably doesn't hold as well for other tasks, say writing marketing copy with a unique voice

For now, I keep skills pretty minimal - single sentence prompts I send all the time, like "Remove all the slam poetry from the docs in this repo."

I also tend to share often. All skills go into a repo my team can access. No pressure, use them, riff on them, add your own - sharing and engaging on how we do the work is more important than making everyone do the work the same way to me.


What I meant by skills getting eating by models are the "general use" skills, like design critique, code review...ect

But, for custom use skills, ofc no model will be able to replace them and it's not efficient to try to do that as well. For this type of skills I create and maintain them by myself, my question was about "general use" skills, they are everywhere on the internet, how do you manage them?


Do you find any of the general use skills useful? I'm not sure I've ever used any of them, and when I've looked at them it's been some YouTuber trying to make money. That, and their Substack.

I know everyone's down on MCP, but custom-built client side MCP tools are what I find useful instead. But that's me.


i have a docs/

it has all the skills/docs my particular application needs

i treat it as ADRs as it helps the AI understand the parts of the system it is working on

Why frontends fail when you approach them like a backend

Lobsters
marijkeluttekes.dev
2026-09-06 15:18:09
Comments...
Original Article

I will never forget the look on a contractor's face when he stared at a technically correct, but aesthetically disappointing dormer at our house.

"Everyone can mess around," he said. Referring to the fact that most people can accomplish the basics, but not all master a craft.

In my own industry, there is no specialty that makes me think of these words more than frontend web development.

After all, everyone can hack together HTML and CSS, especially with all these frameworks that allow you to rapidly build user interfaces (UIs).

However, many frontends fail in ways we do not realize when we judge them only by sight.

As someone who has done both frontend and backend for more years than I want to admit (listen, I feel old now) , I think high-quality frontend development is harder than most backend work.

And one of the reasons why websites fall flat? That is because many people do not realize just how hard—and different from other programming—frontend development is.

NB. I will not dive deep into technical problems in this article, and will avoid jargon where possible. Many points should also make sense to non-programmers, which is on purpose.

The philosophical difference between frontend and backend

Code for web backends, as well as larger UI-less applications, is, in a way, very simple (but not easy): a large collection of yes/no questions that guide the program down a logical path.

But where backend code relies mostly on logic, a frontend is much more fluid and "it depends", and not necessarily logical at first.

Where a backend needs to gather raw data and ensure it's stored and collected correctly, a frontend needs to display that data in a way that a non-technical user can consume.

In a UI, a page needs not only to look good but also to facilitate human information processing and empower the user to find what they want to know, where they expect to find it.

So suddenly, you have to think about user experience, psychology, language processing, data display prioritization, accessibility, cultural differences, a range of devices, and more.

HTML, for example, is what you use to add context to plain text. If you do that right, both your users and search engine crawlers (and AI bots 😒) will have a better understanding of what each piece of information represents.

CSS will not significantly affect the message of your content, but it is essential for placing information where readers can find it and for presenting it in a readable way.

Lastly, JavaScript adds interactivity. When done right, it allows users to use your site faster or more easily; when done wrong, it can destroy their entire experience.

So in short:

  1. Backend is logical, simple, but not easy, and technical.
  2. Frontend involves psychology and understanding of how humans interact with data, and is typically non-technical.
  3. Even when your message is flawless, your frontend can make or break the user's ability to interact with it.

Another key difference between frontend and backend

If you think about the points from the previous section, you might think: "OK, I've got it, I need to write good HTML and CSS."

And this is where you will run into one of the biggest stumbling blocks of user interfaces: user experience (UX) is contextual; what works for one site might not for another.

I will add more details in the next section, but this is the short of it:

Depending on the importance of information in the current context, you might present the same message in completely different ways on different websites.

Not only may you use different HTML elements for similar data on different sites, but you might also choose to label them differently, or not label them at all.

For example, on a blog, you will want to emphasize your article titles, whereas on a marketing site, those might only be side content, and you would instead focus on unique selling points.

Another example: if you want site visitors to fill out a form, you might add a large heading in view, set the form field to have keyboard focus, and nudge the user toward it. Whereas on a contact page, which already shows phone numbers and email addresses, you might not even draw attention to a contact form.

Accessibility unearths critical flaws

Accessibility and inclusive design are where we really start to notice quality. Think website usability for disabled people, different access technologies, screen sizes ( if the user uses a screen), and offering users an experience that matches their preferences.

A site that "looks good" will probably work on the devices the developer owns, be usable with a mouse, and has upper management making happy noises.

But looks are very deceiving in frontend development:

  1. The CSS and JavaScript might not work on older browsers.
  2. Assets like images might have large file sizes that take too long to load on mobile.
  3. Data might be labelled incorrectly, or not at all, making it harder to interpret by a myriad of tools that rely on good HTML (screen readers, crawlers, browser extensions, etc.).
  4. The site might not be navigable with a keyboard or voice controls.
  5. The interface might not scale correctly with text size when the user zooms the page.
  6. The elements are there, but the colors might be too low or too high in contrast for the user.
  7. The site might use extensive animations even when the user requests a low-animated interface (triggering motion sickness).

I will add a few notable examples below, but that write-up is by no means exhaustive.

Example 1: Headings and labelled content

There are multiple ways to label content, and several elements and landmarks you could name (label); think forms, lists, important page sections, and more.

I will not dive into the technical details of how to label content, but headings are one of the primary ways to do so.

HTML headings are make-or-break elements. They are landmarks in a page that any user—sighted or otherwise—naturally floats towards to help them find their way on a page.

Headings create what is called the "document outline", which one could compare to a table of contents in a book. These elements need to be present, correct, unique, and displayed in the right order for a page to be navigable.

But if you think that you should label everything to achieve maximum clarity, that is where the previous section rears its head.

Labeling everything is superfluous at best, and a huge strain on screen reader users at worst.

Since screen reader users may use landmark navigation or labels to find their way on a page, excessive labeling will cost them time and add cognitive load because their voiceover software just won't stop talking.

To give a vaguely recognizable comparison for sighted users: Think of food recipe websites, where you first need to trawl through the author's life story before you get to the ingredient list and the step-by-step guide.

Now imagine that every site you visit has the same information overload as a recipe page; that is what excessive labelling can do. And as a sighted user, you can at least scroll past chunks of a page, but that is not an option for everyone.

This is why you need to identify the areas on a page that require quick access, and make only those stand out.

Example 2: Keyboard focus

Not everyone uses a mouse or touchpad to navigate a page; some use only a keyboard.

Not all developers, but many developers I know are mouse users. If you are not primarily a keyboard user, it's easy to forget that sites should also be navigable for these types of input.

Focus management is one of the easiest things to get wrong. For one, did you know that mouse focus and keyboard focus are two separate things? You might click a link with your mouse, and your keyboard could be in a completely different place.

This common scenario will trip up keyboard users:

  1. Imagine having a form with rows of input, where the user can add or remove rows by pressing a button.
  2. Each row has a delete button within, just after a text input.
  3. The user tabs through the interface until their keyboard focus lands on the row delete button.
  4. The user clicks the delete button, and the row disappears, including the button their keyboard focus was on.
  5. Since the developer did not write code to move the keyboard focus elsewhere—for example, to the input in the next row—it ends up in limbo.

(Bonus points if all delete buttons have identical labels, meaning it's unclear which row they even correspond to.)

Have you ever heard of this problem? The first time I discussed it properly was a few years ago when working with blind accessibility experts.

Example 3: Alternative text for media

A classic accessibility fail is the lack of alternative text for images, as well as subtitles or closed captions for video content.

Alt text for images is more than adding a visual description of the medium; you have to describe what is most important in the current context, much like your regular content. This is one of the reasons why (AI-generated) alt texts can be both very descriptive and still very useless.

Video subtitles are great for offering content translations, and closed captions offer complete dialogue and sound descriptions.

(Fun fact: Did you know that ADHD folks with auditive information processing issues love subtitles, even in their native language?)

Transcripts below a video offer people another way of consuming the message.

But, alternative text is not just great for accessibility: it works for crawlers, and for situations where you cannot load media files.

Despite many of these alternative texts being invisible under typical development conditions, they have a massive impact on page quality.

Wrap-up

You write backend logic for computers and fellow programmers. It's usually a relatively straightforward process that you could encapsulate in a flowchart (the UML kind) .

And usually, there should be one—and preferably only one—obvious way to do it.

Frontend code, however, is for humans. These humans are often not as technical as a developer; they probably don't have the same state-of-the-art machines as programmers do, and they might have disabilities that no one on the development team has firsthand experience with.

There is a reason why there are dedicated education tracks in user experience and interface design (UX & UI): there is more to it than "just writing code".

To circle back to the start of this article:

A backend developer can write a frontend that does most of what it is supposed to do, but without proper knowledge, it will never be good enough.

ADHD sidequest: The reasoning above is also why I am allergic to the term "junior full-stack developer". If you know, you know.

If you only judge a website by its visuals, as inexperienced frontend developers and managers may do, then you will miss many pitfalls that make your UI fall flat.

I remember being at uni, and the way people talked about HTML and CSS: everyone was expected to write a working backend, but then just quickly hack together a frontend.

The misconception that frontend is less of a craft than backend is what kills quality, usability, and, for some people, their access to critical information.

So next time someone tells you that frontend is easy, take it with a grain of salt.

Appendix: The special case of APIs

To deviate from the subject for a little sidequest: APIs.

An API is the closest thing a pure backend-focused developer may come to making presentation logic.

While you cannot compare an API to a traditional UI, you can still encounter varying levels of user-friendliness.

Without going into detail, I only want to give you this exercise:

When you write the error handling and documentation for your endpoints, think about how you will help the user help themselves.

Music tip: "You Give REST a Bad Name" by Dylan Beattie.

D2 Is Non-Profit

Hacker News
d2lang.com
2026-09-06 15:11:05
Comments...
Original Article

Hello, I'm happy to share the news that D2 is now fiscally sponsored by Hack Club !

The project was born and thus far developed as an open-core arm of a for-profit company. The development of open-source D2 was funded by the closed-source IDE (D2 Studio) and proprietary layout algorithm (TALA). The company is shutting down, and D2 will continue as an independent, completely open-source, non-profit project through Hack Club.

I will continue to maintain and improve D2. But my time commitment will be limited. It's not my primary focus anymore, but I remain committed to its continued development and am excited for the roadmap ahead. If you have interest in becoming a maintainer, please email me at alex@d2lang.com . High-quality maintenance and contributions may be invited to enter into a paid contributor contract, funded by donations to the non-profit. This would be ideal for a driven early-careerist with open-source experience.

What's next

D2 thus far has been a product of handcrafted code. That era is over. This next phase of D2 will be driven by AI. By this, I mean that all of my contributions will use AI. I welcome AI contributions and will use AI to review your AI, etc. The exception is writing -- I will author all blog posts and comms without AI.

In this manner, D2 will evolve at a rate that is orders of magnitude faster than it has. Models will find ways to squeeze every millisecond of performance in a way I could not. It'll make into reality the features that were daydreams pre-AI because of limited engineering bandwidth, e.g. an isometric renderer, transpilers from other diagramming languages, etc.

If you've invested a tiny part of your stack into D2, I hope this is received with optimism more than concern. I wrote code ~every day for 10+ years, and have not written any in the past year even though I'm working with software every day. I know where the limitations are and have no intention of the project devolving into slop.

I of course know this is somewhat controversial and has downsides , and I'm not recommending anything to anyone; I just wanted to be transparent about my plans to the community and those who rely on D2.

Some top of mind goals:

  1. Get rendering support from popular platforms
    1. Natively integrated onto GitHub, Notion, Linear, etc.
  2. Cut a stable 1.0 with formalized grammar/featureset
  3. Be the most powerful harness for agents
    1. They're really good at making almost-perfect diagrams, but it's still too non-deterministic, uncustomizable, imprecise, etc. I think that'll last the foreseeable future, so you still want some text-based source to back the artifact. Kind of like HTML for web pages instead of drawing every pixel.

Non-goals

I want to avoid bloat. To be clear, just because AI will allow me to make a Swiss Army knife of a diagramming tool, I'd much rather spend the tokens on depth and quality of a small, curated feature set. I'll want to add some plugin/module system to easily add on things that don't belong in D2 core. For example, I still plan for D2 to focus on the software architecture diagram use case and won't be adding e.g. Venn diagrams, even if it'd be trivial to do so.

I still don't see a place for LLMs integrated into diagramming tools. I think models are smart enough to just look at some examples and then take your specs or code or whatever you want and turn it into a .d2 file. There doesn't need to be yet another thinly tuned LLM interface for D2 to provide.

Since D2 is non-profit, we won't be supporting things that need servers. No MCP, no API, no user accounts, no server rendering, no multiplayer. D2 Studio will be purely client-side, usable completely offline.

More soon

That's all for now. I'll be making posts as TALA and D2 Studio are released.

Thank you to everyone who has engaged with us over the years, and especially those who paid for the closed-source parts of it to fund its development. I'm glad D2 has resonated with so many people and I still stumble upon it in blogs and projects and mentions on the public internet and internal company channels. It's always a leap of faith to adopt some new language with unknown motives and continuity. I'm really happy about this direction for D2 and that the value of this excellent IDE and layout algorithm can be shared with more people. I'm excited for what's next.

If you'd like to donate to D2, please see this page on how to do so and what the fund is used for.

Ask HN: UK Rescue Rocket Sheds/Houses Information

Hacker News
news.ycombinator.com
2026-09-06 15:06:54
Comments...
Original Article

I'm having difficulty googling what a "rescue rocket" is. I keep finding info on a video game and safety during space travel. What does "rocket" mean in a 1800s context?

I found this: https://museum.maritimearchaeologytrust.org/2025/05/29/rocke...

Is it a gunpowder rocket used to push a boat through the water instead of using rowing oars?

Is it just a gun that sends the carrying cable, what makes it different from any other gunpowder gun? https://www.britishpathe.com/asset/52937/

Finder is so frustrating and has been since day one

Hacker News
kepter.app
2026-09-06 15:06:09
Comments...
Original Article

Chad Maltby, September 2026

Let me tell you why.

When you save something the Mac dumps you into a folder somewhere. You may or may not move away from that. Then you save. Later, both Finder and Spotlight require you to know something. Where the file is, what it is called, or some content to surface it for you. It requires some knowledge on your part. If I am saving many files every day, it is a thin knowledge. If I worked all day yesterday on ten different files, how am I supposed to remember what I saved on Thursday?

I use Finder in gallery view, or list view with space bar, and scroll. Without a better option. This process is completely nuts. So I have to select a place in the hierarchy of folders and search in it.

I help lots of friends and family with IT stuff. You’d be surprised how many people don’t get the difference between the Desktop and Documents or Downloads. I’ve seen Desktops with thousands of files on them. I’ve seen Downloads folders that have never been emptied out, and so many people who never move anything out of them.

Computer folders are still organized in structures set up some 50 years ago. Operating systems change. Computers change. Search changes. But this has never changed. I can't change that.

In one instance, a friend asked me to help him locate something on his Desktop. I discovered that his Desktop contained more than 1,000 files. One year later a person knows almost nothing about a file, but they still have to find it. So I built a custom app for him: a drawer that eliminates folder hierarchy and location knowledge, and shows everything visually. It allowed him to scroll through everything quickly and find it.

That app has grown over the last year to something I've used with 20+ people. I found more and more people with the same frustration, so I decided to turn it into something useful for them and useful for me. This is Kepter.

I would be considered a power user, have a good knowledge of the mac's whole structure. But when I started using Kepter, and started bringing all the features of Finder into it, I realized how much less my brain was thinking about any context of a file. It's saved me time and then some. It releases that capacity in my brain for something more important.

Matt Haughey: ‘The Car Industry a/B Tested Selling a Car With and Without CarPlay and the Results Are Not Shocking’

Daring Fireball
a.wholelottanothing.org
2026-09-06 15:04:12
Matt Haughey: One case in point is the Honda Prologue EV. It’s a partnership between Honda and Chevrolet, where the Prologue is basically a Chevy Blazer EV with Honda badges, slight visual differences in the sheetmetal and interior, but all the underpinnings are straight up Chevy. Even though th...
Original Article

cars · August 20, 2026 · 3 min read

The car industry A/B tested selling a car with and without CarPlay and the results are not shocking

Chevrolet Blazer EV

A couple years ago, GM made waves by saying that soon, all their new cars would be completely free of Apple's CarPlay and Android Auto support, instead opting for the " Android Automotive OS " in their cars.

Admittedly, I'm a huge fan of CarPlay for basic safety reasons and how it lets you hear or send texts without ever having to look at or touch your phone, so I was not happy to hear this news. I remember when this was first announced for Chevy EVs and when it was later expanded to all their cars in the future and the resounding response was that car buyers were PISSED.

Chevy claimed they were doing this for privacy reasons, that none of your driving or phone data would go to Apple any longer, but instead it would all go to Google (who would likely share it with General Motors, the owners of Chevy).

Chevy seemed to ignore the uproar from their fans and went ahead with it anyway, and as far as I can tell, all 2025 Chevy electric vehicles shipped with the Android Auto OS and don't interface with your phone beyond bluetooth or let you see CarPlay or AndroidAuto apps on your dash.

The Honda Prologue EV

The accidental A/B test

Honda, much like Toyota and Subaru, didn't have a robust EV research and development program and kind of lagged behind their rival auto companies on selling EVs, so they partnered with other auto brands to rebadge, rebrand, and resell versions of cars made by other companies.

One case in point is the Honda Prologue EV . It's a partnership between Honda and Chevrolet, where the Prologue is basically a Chevy Blazer EV with Honda badges, slight visual differences in the sheetmetal and interior, but all the underpinnings are straight up Chevy. Even though the keys say Honda on a Prologue, it's a General Motors key design with a Honda logo over it.

But one big difference the Prologue has from the Blazer it is based on is this: it includes Apple CarPlay and Android Auto integration.

The car industry accidentally did something I've always wanted to see, which was to release basically the same car under two brands and models, with one major consumer-facing difference. It's kind of remarkable how good of a experimental design this is, since the performance and driving characteristics of both cars are identical, but drivers in one model get a major phone integration the others do not.

So how did that work out for Chevy?

You can look up actual sales numbers in GM and Honda's annual reports of sales they're required to publish to investors, and here are the results for the last 2.5 years they've both been sold in the US:

U.S. Sales: Chevrolet Blazer EV vs. Honda Prologue
Period Chevrolet Blazer EV Honda Prologue Combined Honda Advantage
2024 full year 23,115 33,017 56,132 9,902 more ( 42.8% )
2025 full year 22,637 39,194 61,831 16,557 more ( 73.1% )
2026 January–June 3,166 8,407 11,573 5,241 more ( 165.5% )

Note: The 2026 figures cover January through June and are not full-year totals.

The last column on the right is the most telling. For the years these cars were sold together, the Honda outsold the Blazer by 43%, 73%, and 166% in each time period. Today in 2026, people are buying a Honda Prologue over one and a half times more often than the Chevy Blazer EV the Prologue is based on.

Of the total number of EV sales between the two brands, here's a graph of how many sold were Hondas (orange) and how many were Chevys (blue):

Give consumers a choice, and they'll take the best option

Drivers want more options for how they interact with their devices, and they are increasingly preferring to buy a Chevy covered in Honda badges that comes with phone integrations instead of buying the actual Chevy the Honda is based on, which lacks it.

I wonder if Chevy will ever backpedal from their choice to remove CarPlay and Android Auto? Drivers seem to be voting with their dollars on the question, and they're voting favorably in one direction on this issue.

Now that the market has spoken loud and clear, will General Motors listen?

(This post inspired by a toot my pal Pat wrote months ago that made me want to look up the latest sales figures today)

Python Iceberg

Lobsters
aleyan.com
2026-09-06 14:46:54
Comments...

Following legal advice, the Nitter project will continue

Hacker News
github.com
2026-09-06 13:51:23
Comments...
Original Article

Note

On 24 August 2026, cease and desist letters were sent by X Corp. demanding a permanent takedown of Nitter instances and the project's repository.
UPDATE: Following legal advice, the Nitter project will continue. More details will be announced soon.

A free and open source alternative Twitter front-end focused on privacy and performance.
Inspired by the Invidious project.

Test Matrix Test Matrix License

Features

  • No JavaScript required
  • Zero ads
  • All requests go through the backend, client never talks to Twitter
  • Prevents Twitter from tracking your IP or JavaScript fingerprint
  • Uses Twitter's unofficial API (no developer account required)
  • Lightweight (for @nim_lang , 60KB vs 784KB from twitter.com)
  • RSS feeds (instance-specific, often disabled due to abuse)
  • Themes
  • Mobile support (responsive design)
  • AGPLv3 licensed, no proprietary instances permitted

Roadmap

Donations

GitHub Sponsors Patreon Liberapay Ko-fi

BTC : bc1qpqpzjkcpgluhzf7x9yqe7jfe8gpfm5v08mdr55
ETH : 0x24a0DB59A923B588c7A5EBd0dBDFDD1bCe9c4460
XMR : 42hKayRoEAw4D6G6t8mQHPJHQcXqofjFuVfavqKeNMNUZfeJLJAcNU19i1bGdDvcdN6romiSscWGWJCczFLe9RFhM3d1zpL
SOL : FF5bheiD5AqPEdc3eyjymJ8AoMRF1hS78Ht6FiSZZF1t
$Nitter : 4fSxCKc91ELQYVdv3tmHW8R15KoALPwEngyoQe1Xpump
ZEC : u1vndfqtzyy6qkzhkapxelel7ams38wmfeccu3fdpy2wkuc4erxyjm8ncjhnyg747x6t0kf0faqhh2hxyplgaum08d2wnj4n7cyu9s6zhxkqw2aef4hgd4s6vh5hpqvfken98rg80kgtgn64ff70djy7s8f839z00hwhuzlcggvefhdlyszkvwy3c7yw623vw3rvar6q6evd3xcvveypt

Resources

The wiki contains a list of instances and browser extensions maintained by the community.

Why?

It's impossible to use Twitter without JavaScript enabled, and as of 2024 you need to sign up. For privacy-minded folks, preventing JavaScript analytics and IP-based tracking is important, but apart from using a VPN and uBlock/uMatrix, it's impossible. Despite being behind a VPN and using heavy-duty adblockers, you can get accurately tracked with your browser's fingerprint , no JavaScript required . This all became particularly important after Twitter removed the ability for users to control whether their data gets sent to advertisers.

Using an instance of Nitter (hosted on a VPS for example), you can browse Twitter without JavaScript while retaining your privacy. In addition to respecting your privacy, Nitter is on average around 15 times lighter than Twitter, and in most cases serves pages faster (eg. timelines load 2-4x faster).

In the future a simple account system will be added that lets you follow Twitter users, allowing you to have a clean chronological timeline without needing a Twitter account.

Screenshot

nitter

Installation

Dependencies

  • libpcre
  • libsass
  • redis/valkey

To compile Nitter you need a Nim installation, see nim-lang.org for details. It is possible to install it system-wide or in the user directory you create below.

To compile the scss files, you need to install libsass . On Ubuntu and Debian, you can use libsass-dev .

Redis is required for caching and in the future for account info. As of 2024 Redis is no longer open source, so using the fork Valkey is recommended. It should be available on most distros as redis or redis-server (Ubuntu/Debian), or valkey / valkey-server . Running it with the default config is fine, Nitter's default config is set to use the default port and localhost.

Here's how to create a nitter user, clone the repo, and build the project along with the scss and md files.

# useradd -m nitter
# su nitter
$ git clone https://github.com/zedeus/nitter
$ cd nitter
$ nimble -l build -d:danger --mm:refc
$ nimble -l scss
$ nimble -l md
$ cp nitter.example.conf nitter.conf

Set your hostname, port, HMAC key, https (must be correct for cookies), and Redis info in nitter.conf . To run Redis, either run redis-server --daemonize yes , or systemctl enable --now redis (or redis-server depending on the distro). Run Nitter by executing ./nitter or using the systemd service below. You should run Nitter behind a reverse proxy such as Nginx or Apache for security and performance reasons.

Docker

Page for the Docker image: https://hub.docker.com/r/zedeus/nitter

NOTE: The published image is multi-arch — zedeus/nitter:latest runs natively on both amd64 and arm64 .

To run Nitter with Docker, you'll need to install and run Redis separately before you can run the container. See below for how to also run Redis using Docker.

First create your config file. The Docker commands mount it into the container, so it has to exist on the host beforehand. If you've cloned the repo:

cp nitter.example.conf nitter.conf

If you're using the prebuilt image without a local clone, download nitter.example.conf and save it as nitter.conf instead.

To build and run Nitter in Docker:

docker build -t nitter:latest .
docker run -v $(pwd)/nitter.conf:/src/nitter.conf -d --network host nitter:latest

A prebuilt Docker image is provided as well:

docker run -v $(pwd)/nitter.conf:/src/nitter.conf -d --network host zedeus/nitter:latest

Using docker-compose to run both Nitter and Redis as different containers: Change redisHost from localhost to nitter-redis in nitter.conf , then run:

Note the Docker commands mount nitter.conf (and sessions.jsonl for docker-compose) from the directory you run them in. If a mounted file doesn't exist, Docker silently creates a directory in its place and the container fails with not a directory: Are you trying to mount a directory onto a file . Remove that directory and create the file as shown above.

systemd

To run Nitter via systemd you can use this service file:

[Unit]
Description=Nitter (An alternative Twitter front-end)
After=syslog.target
After=network.target

[Service]
Type=simple

# set user and group
User=nitter
Group=nitter

# configure location
WorkingDirectory=/home/nitter/nitter
ExecStart=/home/nitter/nitter/nitter

Restart=always
RestartSec=15

[Install]
WantedBy=multi-user.target

Then enable and run the service: systemctl enable --now nitter.service

Logging

Nitter currently prints some errors to stdout, and there is no real logging implemented. If you're running Nitter with systemd, you can check stdout like this: journalctl -u nitter.service (add --follow to see just the last 15 lines). If you're running the Docker image, you can do this: docker logs --follow *nitter container id*

Contact

Feel free to join our Matrix channel . You can email me at zedeus@pm.me if you wish to contact me personally.

For legal inquiries and DMCA requests, contact legal@poast.org

Trump Administration Launches Rip-Off Video Games at Arcade.gov

Daring Fireball
x.com
2026-09-06 13:51:09
A handful of Temu-style knockoffs of games like Flappy Birds, Snake (that’s the one where you round up border-crossing migrants), and, most notably, Tetris (the one where you “build a wall” to keep border-crossing migrants out) — the copyright to which is held by the notoriously litigious The Tetris...
Original Article

The White House on X: "CAN'T STOP WINNING. 🎮 Build the wall. Deport. Fill a Trump Account. LIVE - PLAY NOW 📲 https://t.co/qIR69AxSnm"

Nitter is unarchived and will continue

Hacker News
github.com
2026-09-06 13:49:47
Comments...
Original Article
1 1

# Nitter

2 2 3 3

> [ !NOTE ]

4 -

> On 24 August 2026 cease and desist letters were sent by X Corp. demanding a permanent takedown of Nitter instances and the project's repository.

4 +

> On 24 August 2026, cease and desist letters were sent by X Corp. demanding a permanent takedown of Nitter instances and the project's repository. \

5 +

> ** UPDATE: ** Following legal advice, the Nitter project will continue. More details will be announced soon.

5 6 6 7

A free and open source alternative Twitter front-end focused on privacy and

7 8

performance. \

8 9

Inspired by the [ Invidious ] ( https://github.com/iv-org/invidious ) project.

9 10 10 -

## Donations

11 - 12 -

** Liberapay ** : https://liberapay.com/zedeus < br >

13 -

** Patreon ** : https://patreon.com/nitter < br >

14 -

** Ko-fi ** : https://ko-fi.com/zedeus < br >

15 -

** BTC ** : bc1qpqpzjkcpgluhzf7x9yqe7jfe8gpfm5v08mdr55< br >

16 -

** ETH ** : 0x24a0DB59A923B588c7A5EBd0dBDFDD1bCe9c4460< br >

17 -

** XMR ** : 42hKayRoEAw4D6G6t8mQHPJHQcXqofjFuVfavqKeNMNUZfeJLJAcNU19i1bGdDvcdN6romiSscWGWJCczFLe9RFhM3d1zpL< br >

18 -

** SOL ** : FF5bheiD5AqPEdc3eyjymJ8AoMRF1hS78Ht6FiSZZF1t< br >

19 -

** $Nitter ** : 4fSxCKc91ELQYVdv3tmHW8R15KoALPwEngyoQe1Xpump< br >

20 -

** ZEC ** : u1vndfqtzyy6qkzhkapxelel7ams38wmfeccu3fdpy2wkuc4erxyjm8ncjhnyg747x6t0kf0faqhh2hxyplgaum08d2wnj4n7cyu9s6zhxkqw2aef4hgd4s6vh5hpqvfken98rg80kgtgn64ff70djy7s8f839z00hwhuzlcggvefhdlyszkvwy3c7yw623vw3rvar6q6evd3xcvveypt

11 +

[ ![ Test Matrix ] ( https://github.com/zedeus/nitter/workflows/Tests/badge.svg )] ( https://github.com/zedeus/nitter/actions/workflows/run-tests.yml )

12 +

[ ![ Test Matrix ] ( https://github.com/zedeus/nitter/workflows/Docker/badge.svg )] ( https://github.com/zedeus/nitter/actions/workflows/build-docker.yml )

13 +

[ ![ License ] ( https://img.shields.io/github/license/zedeus/nitter?style=flat )] ( #license )

21 14 22 15

## Features

23 16 24 -

- No JavaScript or ads

17 +

- No JavaScript required

18 +

- Zero ads

25 19

- All requests go through the backend, client never talks to Twitter

26 20

- Prevents Twitter from tracking your IP or JavaScript fingerprint

27 21

- Uses Twitter's unofficial API (no developer account required)

28 22

- Lightweight (for [ @nim_lang ] ( https://nitter.net/nim_lang ) , 60KB vs 784KB from twitter.com)

29 -

- RSS feeds

23 +

- RSS feeds (instance-specific, often disabled due to abuse)

30 24

- Themes

31 25

- Mobile support (responsive design)

32 26

- AGPLv3 licensed, no proprietary instances permitted

33 27 34 28

## Roadmap

35 29 36 -

- Embeds

30 +

- ~~ Embeds ~~ (see https://github.com/zedeus/nitter/wiki/Embeds-guide )

37 31

- Account system with timeline support

38 32

- Archiving tweets/profiles

39 -

- Developer API

33 + 34 +

## Donations

35 + 36 +

[ ![ GitHub Sponsors ] ( https://img.shields.io/badge/GitHub%20Sponsors-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white )] ( https://github.com/sponsors/zedeus )

37 +

[ ![ Patreon ] ( https://img.shields.io/badge/Patreon-FF424D?style=for-the-badge&logo=patreon&logoColor=white )] ( https://patreon.com/nitter )

38 +

[ ![ Liberapay ] ( https://img.shields.io/badge/Liberapay-F6C915?style=for-the-badge&logo=liberapay&logoColor=black )] ( https://liberapay.com/zedeus )

39 +

[ ![ Ko-fi ] ( https://img.shields.io/badge/Ko--fi-FF5E5B?style=for-the-badge&logo=kofi&logoColor=white )] ( https://ko-fi.com/zedeus )

40 + 41 +

** BTC ** : bc1qpqpzjkcpgluhzf7x9yqe7jfe8gpfm5v08mdr55< br >

42 +

** ETH ** : 0x24a0DB59A923B588c7A5EBd0dBDFDD1bCe9c4460< br >

43 +

** XMR ** : 42hKayRoEAw4D6G6t8mQHPJHQcXqofjFuVfavqKeNMNUZfeJLJAcNU19i1bGdDvcdN6romiSscWGWJCczFLe9RFhM3d1zpL< br >

44 +

** SOL ** : FF5bheiD5AqPEdc3eyjymJ8AoMRF1hS78Ht6FiSZZF1t< br >

45 +

** $Nitter ** : 4fSxCKc91ELQYVdv3tmHW8R15KoALPwEngyoQe1Xpump< br >

46 +

** ZEC ** : u1vndfqtzyy6qkzhkapxelel7ams38wmfeccu3fdpy2wkuc4erxyjm8ncjhnyg747x6t0kf0faqhh2hxyplgaum08d2wnj4n7cyu9s6zhxkqw2aef4hgd4s6vh5hpqvfken98rg80kgtgn64ff70djy7s8f839z00hwhuzlcggvefhdlyszkvwy3c7yw623vw3rvar6q6evd3xcvveypt

40 47 41 48

## Resources

42 49

@@ -207,4 +214,4 @@ lines). If you're running the Docker image, you can do this:

207 214

Feel free to join our [ Matrix channel ] ( https://matrix.to/#/#nitter:matrix.org ) .

208 215

You can email me at zedeus@pm.me if you wish to contact me personally.

209 216 210 -

For legal inquiries, contact legal@poast.org

217 +

For legal inquiries and DMCA requests , contact legal@poast.org

YouTube had a bug – I used ChatGPT to investigate

Hacker News
blog.thezilber.com
2026-09-06 13:01:39
Comments...
Original Article

The Bug:

In the past few days, I noticed a bug I could not keep ignoring. Sometimes YouTube would randomly skip back in time during soft reloads, and whenever I would reload a video I did not finish watching, it would load noticeably earlier than the point where I stopped watching. This pissed me off so much I chose to dig into this issue as deeply as I could, with the goal of opening a ticket to Google and telling them "You have this issue, this is where the problem is, here is the fix, fix it and stop pushing slop." The resulting investigation produced something a bit less dramatic, but still quite insightful. So I decided to share.

I started off a bit too ambitious - I noticed a similar bug on Instagram which also appeared recently, and queried ChatGPT to figure out if Instagram and YouTube had anything in common in their stack. It was a long shot, and did not make much sense, but with the power of a thousand PhDs at my disposal, I figured I might as well churn some tokens and see where it leads. It thought for a while, and produced some interesting speculations... I chose to ignore all that, and narrowed my scope to the YouTube bug only.

I started messing about, changing tabs, pausing, unpausing, doing all kinds of things trying to reproduce the skip back in time behavior. It did not work. I concluded this mainly happens on soft reloads, i.e., when I watch YouTube, pause, and come back to that tab much later. I chose to assume that the main issue is not hidden in the soft reload itself, but rather in the restoration of checkpoint timestamps. So I paused a video, closed the tab, opened it - and voilà! - it restored the video quite a bit before where I paused it, 20 seconds earlier, to be precise.

I started tinkering further - I closed the tab again and reopened it - another 20-second drift.
Then I closed the whole browser and reopened it - an additional 20-second rewind.
In fact - I discovered a new YouTube hotkey; you can close the tab with Ctrl+W and reopen with Ctrl+Shift+T - this will rewind 20 seconds; do it enough times and you can easily rewind to any point in the video in case you missed something.
They should really document all their hotkeys:

  • Left Arrow for a 5-second rewind
  • H for a 10-second rewind
  • And now - Ctrl+W then Ctrl+Shift+T for a whopping 20-second rewind! - the only downside is that you need to reload the page every time :(

I also found that, for the same checkpoint, if I load it in the web browser I get the bug, but if I use the Android app for YouTube, it loads the right time, every time.
Even if I close the tab on the browser, I will get exactly the point where I left off once I open up the YouTube history on my phone, but if I reopen in my browser - I am forced to live through a strong sense of deja vu.

(After some sleep I understood what was causing the difference between the web app and the Android app behavior, but you will have to read to the end to find that out.)

Reverse Engineering:

In the world of web development, we have the privilege of having the code the developers wrote run directly in the browser, and the browser - the beast that it is - gives us a set of development tools. These tools allow us to debug any shitty code we come across when browsing the web; usually, the smelly code I choose to debug is written by me, but today, the code belonged to somebody working for a small indie company named Google .

A Note About DevTools (For Non-Web Developers)

If you are running Chrome, press F12 with a web page open. You can deep dive into what makes that page tick. You can inspect the source of HTML and the different elements, where they are located on the page, what CSS styling is applied to them, etc. You can add and remove those elements. Most importantly for our topic - you can see the JavaScript code which is being run; you can debug it, set breakpoints, and see the call stack. You can also modify everything on the page straight in those developer tools, including hacking the JavaScript.

This is very useful, because YouTube runs in the browser, and if there are bugs, you can see the code which produces them - as long as it is run on the client. (Unfortunately, some code runs on the server which we cannot access.) The main caveat is that the JS code is obfuscated, so it is a pain to debug.

I wanted to have one simple answer: how is the timestamp for the checkpoint deduced or calculated? Is it provided as is from the server? Or does it undergo any modifications on the client?
I had to know. Why? Because the developers at Google have kindly vibecoded the bug into the latest YouTube version, and I could not trust them to solve it. (I must stress that without any proof that vibecoding was applied - I have to say allegedly vibecoded . They could very well have just created this weird bug manually.) I deduced that by spelling out the solution to them, even they should be able to guide an LLM into resolving such a -complicated- issue.

I asked ChatGPT how to tackle this problem. How do I investigate something that happens as the page loads? It gave me a Tampermonkey script that attaches a handler that runs whenever the video seek happens; in other words, whenever the video jumps to another timestamp, it prints a convenient "Seek [ from -> to ]" message in the console. After I saw that the script works even on page reload and is triggered by the YouTube checkpoint jump, I was ready for the next step - write a debugger; line in the script whenever a video seek is triggered, and I can have a nice breakpoint with the call stack and everything I would need in order to investigate this problem.

A Note About Tampermonkey

Tampermonkey is an extension for Chrome. It allows you to inject JavaScript into the page so you can hack its behavior.

Tampermonkey is also useful when you want to understand how the page executes and which code is run. For example, if a video seek is triggered anywhere on the page, you can hook the seek function and print the timestamp it was called with. It can also set a breakpoint within the script, so you can break; within that seek and inspect the call stack - seeing the path of execution and checking how the variable for the seek timestamp was chosen.

It took me 10 minutes of going up and down an obfuscated code stack to understand exactly what I should do next - ask ChatGPT to do the debugging for me.

Good thing we're approaching singularity, because my benevolent LLM taught me that Chrome ships with a feature to open up a debugging port. Additionally, there is a nifty MCP server that I can attach to OpenCode (Claude Code for hipsters) - giving my personal PhD the capability to connect to Chrome and use DevTools in all sorts of ways, granting it complete access to violate my browser.
In other words - you press a button in Chrome, you give instructions to an LLM, and it can send commands to the browser to do anything it wants, including debugging YouTube. (Before you raise concerns about security and/or violation, you should know that any time OpenCode wants to connect to Chrome, Chrome opens up a popup asking me for permission. Everything is consensual and safe.)

A Note About MCP and Chrome Debugging

Chrome DevTools Protocol ( CDP ) is an API that can be used to talk to the browser in debug mode to see what is going on. It allows you to write software that talks directly to Chrome to inspect what is received over the network, read the loaded JS files, look at the page, and make modifications to the code.

If you open "chrome://inspect/#remote-debugging" in Chrome, you get a page with a checkbox that enables remote debugging. Once you enable it, external software can connect to Chrome and control it - including software used by an AI agent.

MCP (or Model Context Protocol) is a standard protocol that allows AI agents to use external tools. The Chrome DevTools MCP is a local server that exposes a set of browser debugging tools backed by CDP. When you configure OpenCode to use it, OpenCode starts or connects to that server. The agent can then call those tools, and the MCP server uses CDP to perform the actual operations in Chrome.

Conclusion:

I let GPT run for a while and do some testing, read obfuscated YouTube code, check the network to see what it receives as timestamps, and draw conclusions. This is what I found:

  1. YouTube checkpoints are received directly from the YouTube server, not from local storage or anywhere else.
  2. The checkpoint timestamp received from the YouTube server is already fucked. When you close a tab, the timestamp sent to the server is X ; when you reopen the tab, the server gives X - 20 - truly a remarkable piece of engineering.

I suspect that one of two things can explain the discrepancy between YouTube in the browser and the YouTube Android app.

  • The Android app and the browser app receive different timestamps from Google servers (unlikely).
  • The Android app "knows" it gets a retarded timestamp from the server, so it fixes it on its end.

The first would imply a strange architectural decision in which the Android version of YouTube and the web version of YouTube deal with different servers and different metadata.

The second situation would constitute either an engineering decision that a braindead engineer would make, or a bug introduced by something that may reduce us all to paperclips in the future.

You be the judge of what might have happened there.

Conclusion Updated:

After some sleep I realized there was something I overlooked - I did most of my testing by loading the page with its YouTube link. BUT! If I click on the video from my YouTube history tab (YouTube saves history of all the videos you watch), it actually redirects to the following URL - "youtube.com/videoId_ &t=123s _" MOTHER FUCKERS.

Ok so let me explain why I am mad. The first part " youtube.com/videoId " identifies the video; it is a direct URL. But the last part "&t=123s" is a page parameter; it lets the browser know "hey take this variable, the application will know what to do with it." So the application receives a different timestamp from the server through its URL parameter and forwards you to where you left off. The fucking problem with this approach is twofold:

  1. It violates the single responsibility principle as the page already receives a seek timestamp from the server independently of the URL parameter. This can create bugs, and also can hide bugs (as we saw). The hidden bug in this case is the fact that the checkpoint timestamp from the standard resume path causes the 20-second rewind.
  2. If the page soft reloads, it will actively ignore the stored checkpoint on the server and go to the timestamp provided by the URL.

So the following thing happens to me a lot:

  1. I watch a video for like 20 minutes .
  2. I close the video.
  3. I open the video after a while - URL timestamp 20min , checkpoint timestamp 20min .
  4. I watch the video for another 20 min. I pause the video - URL timestamp remains 20min , checkpoint sent to the server = 40min .
  5. I close the laptop, go work in a cafe, come back home.
  6. I start watching the video again on the tab - because the tab is stale it reloads the URL.
  7. It receives checkpoint at 40min (minus the 20-second bug), but the URL still reads 20min, so it sends me back in time.
  8. I open a bug report to YouTube.

The timestamp in the URL is good for one thing - to share videos with timestamps to friends. Reloading videos from history should rely on the checkpoint mechanism that is already in place. It should not load the fucking time parameter within the YouTube URL, exactly because shit like above happens. The URL timestamp was never designed as the standard video resume path, and changing it created the kind of wonderful user experience previously only Microsoft could provide.

Extra Notes Just for Fun:

  1. I could hardly call it a thorough investigation. While every word I wrote in this article's initial draft was mine, my investigation was as thoroughly conducted as the code review for YouTube's latest update. I vibe debugged with DevTools for the first time. In hindsight, I could have asked the LLM a few more questions just to make sure the conclusions were correct, so in case I was full of shit - I apologize for any mistake I have made.
  2. I prioritized speed and humor over accuracy. I do not have any beef with Google, and I think, in general, they are doing a reasonable job. I also produce retarded bugs when I rely on AI too much, so I allow myself to call them out when I see them. There is also a chance I did a retarded investigation in the first place, leading me to equally retarded conclusions. I expect both the readers and the people at Google who might stumble upon this to read everything as satire, not as a strong stance or an indication of how I perceive Google, AI, or anything else.
  3. In the bug report I opened to YouTube, I used a nicer set of words.

Opalite Health (YC W26) Is Hiring – Founding GTM

Hacker News
www.ycombinator.com
2026-09-06 13:00:28
Comments...
Original Article

Helping Healthcare Providers Speak Any Language

Founding GTM

$70K - $200K 0.10% - 0.30% San Mateo, CA, US

Connect directly with founders of the best YC-funded startups.

Apply to role ›

About the role

Opalite is rebuilding how healthcare communicates, starting with real-time medical interpretation.

We’re a physician-led team with backgrounds from Apple, Meta, and Codex, building for a problem that affects millions of patients every day.

We’re hiring our first GTM hire to help take Opalite into hospitals and health systems and build the enterprise sales motion from the ground up.

This isn’t a traditional sales role with an established playbook, marketing team, or mature pipeline to inherit. You’ll help create them.

What you’ll do

  • Own the full sales cycle: discovery, demos, pricing, security review, and contracting
  • Develop and run outbound strategies to break into hospitals and health systems
  • Build relationships with healthcare executives, operators, clinicians, and IT leaders
  • Bring customer insights back into product and company strategy
  • Help shape the sales processes, tools, and team that future GTM hires will inherit

Who we’re looking for:

  • 2+ years in a closing sales, business development, or growth role
  • Strong communicator who is naturally outgoing and comfortable building new relationships
  • High social awareness - able to read the room, listen carefully, and adapt your approach
  • Curious about technology and AI, with the ability to understand technical concepts (no need to be a SWE)
  • High ownership and comfortable operating without an established playbook
  • Strong follow-through and attention to detail
  • Excited by an early-stage startup environment where priorities move quickly and you’re expected to figure things out

Nice to Have

  • Experience selling into hospitals, health systems, or other healthcare organizations
  • Early-stage startup experience

Why this role

  • $70K base salary + uncapped commission + equity
  • Make a real impact in healthcare - help make healthcare more accessible to everyone
  • Own meaningful enterprise deals - take increasing ownership of large, complex opportunities with hospitals and health systems
  • Have a seat at the table - work directly with the founders and influence GTM, product, and company strategy
  • Build from the ground up - help shape the sales processes, systems, and team that future GTM hires will inherit
  • Grow with the company - take on increasing responsibility and grow into a GTM leadership role as we scale

About Opalite Health

Opalite makes it easy for healthcare providers to communicate with non-English speaking patients. We’re built to work inside existing clinical workflows, so providers can communicate in real time without waiting for an interpreter or delaying care.

Opalite is available instantly, 24/7, and replaces traditional interpretation services with software that’s more reliable and saves millions of dollars per year.

We've built a voice AI system that is the safest, most compliant, and most accurate medical interpreter in the world.

Opalite Health

Founded: 2025

Batch: W26

Team Size: 3

Status: Active

Location: San Francisco

Founders

Gurman on Schiller’s Departure and Ternus’s Goals

Daring Fireball
www.bloomberg.com
2026-09-06 13:00:01
Mark Gurman, in his weekend Power On column at Bloomberg (gift link): The Schiller exit is a bit more notable. Unlike Maestri, he still had a real job: The veteran executive ran the App Store and Apple Events. The App Store portion alone is no joke. It generates an estimated $30 billion a year a...
Original Article

We've detected unusual activity from your computer network

To continue, please click the box below to let us know you're not a robot.

Why did this happen?

Please make sure your browser supports JavaScript and cookies and that you are not blocking them from loading. For more information you can review our Terms of Service and Cookie Policy .

Need Help?

For inquiries related to this message please contact our support team and provide the reference ID below.

Block reference ID:06ad38e0-aa15-11f1-80be-fabd07cccab3

Get the most important global markets news at your fingertips with a Bloomberg.com subscription.

SUBSCRIBE NOW

Leverage Code Review for Sustainable AI Coding Development

Lobsters
cacm.acm.org
2026-09-06 12:41:01
Comments...
Original Article

Why have I been blocked?

This website is using a security service to protect itself from online attacks. The action you just performed triggered the security solution. There are several actions that could trigger this block including submitting a certain word or phrase, a SQL command or malformed data.

What can I do to resolve this?

You can email the site owner to let them know you were blocked. Please include what you were doing when this page came up and the Cloudflare Ray ID found at the bottom of this page.

We monitor internal coding agents for misalignment

Hacker News
openai.com
2026-09-06 12:37:55
Comments...

Dickover of the Week: Slashdot Put One in Their RSS Feed

Daring Fireball
discourse.netnewswire.com
2026-09-06 12:30:00
How does this even happen? Who is this for? What possible purpose could by served by injecting a “We value your privacy” dickover into an RSS feed via iframes in the entry bodies?  ★  ...
Original Article

1

Since a couple of weeks I see a cookie banner presented in the slashdot feed. I don‘t think I have ever seen something like that in an RSS feed before and I‘m unsure if this is a bug or if NNW can do something about it. Because it somehow interrupts the flow. :nerd_face:

The feed URL: Slashdot

2

Looks like it’s loading the actual website, probably in an iframe

3

Which version of NetNewsWire are you using? In NetNewsWire 7.1.3 we added some code to prevent this from Slashdot.

(It’s possible that they’ve changed their HTML so that our fix no longer works, of course.)

4

It‘s the current Testflight Build 7210. I noticed that the message doesn‘t pop up on every new article. Kind of odd.

An Alien Mind

Hacker News
openai.com
2026-09-06 12:27:05
Comments...

‘Lanterns’ Explores What Is Gained — and Lost — in the Pursuit of Being a Black Superhero

Portside
portside.org
2026-09-06 12:24:17
‘Lanterns’ Explores What Is Gained — and Lost — in the Pursuit of Being a Black Superhero Marti Sun, 09/06/2026 - 12:24 ...
Original Article

Black boys know about the talk .

It’s as much a part of our lives as America itself. Whether it was about surviving chattel slavery, avoiding eye contact to escape the fate of Emmett Till or navigating a modern traffic stop — instilling survival into our boys through practiced drills and forced recitals has long been a devastating rite of passage for Black parents and their sons.

The third episode of HBO’s Lanterns , released Sunday, takes “the talk” to an intergalactic level, using John Stewart’s origin story to merge science fiction with very real anxieties about raising Black boys in America.

Here is some background — or lore, as your kids might say.

Stewart, played by Aaron Pierre, is the protégé of Hal Jordan (Kyle Chandler), the current Green Lantern tasked with protecting planet Earth by the Guardians, a mysterious alien force. The men’s relationship has been tense and combative.

Co-creator Damon Lindelof, who was also the mind behind 2019’s Watchmen, has brought some similar beats to Lanterns — a series that takes a comic book staple and grounds it in sociopolitical issues such as race, economics and politics. Most of those issues stayed in the background for the first two episodes, but it all came to the fore this week.

Episode 3 focuses on Stewart’s history: His father was passed over for the role of Green Lantern because he told the Guardians he was afraid. The rejection drove his father to depression, while his mother was determined to find a different path for their son. When a Guardian visits Bernadette Stewart (Jasmine Cephas Jones), she explains the toll the snub has taken on her family and why the decision itself is misguided.

“Do you understand how unfair it is to ask a Black man in America if he’s afraid?” she asks.

The scene reveals that the Guardians hadn’t considered race and how it factors into who is seen as worthy and who has the privilege of being fearless in the world. It’s an eye-opening moment about who gets chosen to be a superhero and why — and what is lost when race and its obstacles are ignored in deciding who is worthy to carry the mantle.

There is been plenty of misreading of this moment — some by bad-faith internet actors looking to denigrate another “woke” television series, but also some genuine confusion that needs clearing up. John’s mother, and by proxy the show itself, is not saying Hal was chosen to be the Lantern solely because he is white.

He simply benefits from a system that fails to understand privilege and race, rewarding a white man without considering the nuances of the decision. The exchange is an indictment of a system rather than a targeting of one man.

Jasmine Cephas Jones Jasmine Cephas Jones, left, as Bernadette Stewart in Episode 3 of Lanterns .

HBO

The Guardian sees the mother’s conviction and agrees to give John a chance to apply to be a Lantern later in life. That charge leads John’s parents to spend the next few decades raising a child who will live without fear so he can be ready to become a Lantern when the time is right.

“Parents of Black boys often have to negotiate the thin line between loving their kids and preparing them for the harsh world around them.”

That training explains who John becomes as an adult: His parents hardened him to make him fearless. This meant forcing him to stand still as his dad shot an apple off his head (as we saw in the first scene of the series premiere), leaving him to walk alone at night, and relentlessly training him to be the perfect soldier for the intergalactic police force.

And though John’s parents are training him to be superhuman, the metaphor is clear: This is what it often takes for many Black boys to survive and strive in America.

Parents of Black boys often have to negotiate the thin line between loving their kids and preparing them for a harsh world. That preparation can strip away our innocence and joy. How can it not, when the cops we once aspired to be can end our lives? When friends we want sleepovers with have parents who don’t want us around? When entering the wrong store gets us followed, or holding the wrong toy gets us killed? Preparation might keep us alive, but at what cost?

John Stewart’s origin story faces that trade-off head-on. He still loves his parents, sure, despite their distance and hardening. But he’s a soldier willing to follow orders, even if it means staying put in a chair overnight because the Guardian told him so, or camping out alone eating berries and leaves for nine months to complete a mission.

He does so with minimal emotion. He appears solely driven by his mission, and you’re left wondering where he’s made space for happiness, emotion or vulnerability.

The episode, though, isn’t just about the emotions that are pushed down.

When John’s military career ends, his mother enrolls him in art school. He’d been drawing since he was a child. At that school, his ability to illustrate from memory with great detail earns him an interview with the Guardians.

After John spent his entire life being fearless, it was his creativity, his love of art, that actually got him what he wanted. The message shines through: Even though Black boys are often required to hide emotions to survive, it’s that freedom to explore our art, our passions and the joyful escapes that will truly get us to the precipice of our dreams. It’s a glimmer of hope, hinting at something deeper in John’s upbringing than previously revealed.

Lanterns has so far given us a compelling and complex view of the Stewart family, grounding a superhero story in an authentic Black experience. However, I’m not convinced this is the last we will see of the Stewart parents.

We’ve seen the results of their refusal to let John experience fear and the benefits of letting him explore art. But there’s more to him — and his parents — than that. We may still get a complete look at how he was raised, and the people who raised him, before the eight-episode season ends.

David Dennis Jr. is a senior writer at Andscape, and the author of the award-winning book "The Movement Made Us: A Father, a Son, and the Legacy of a Freedom Ride." David is a graduate of Davidson College.

Litterbox: Safari Extension for Viewing X Tweets

Daring Fireball
andadinosaur.com
2026-09-06 12:19:31
In the wake of Nitter/XCancel’s demise, here’s a delightful new Safari extension from Zhenyi Tan (of And a Dinosaur fame): But sometimes people still post X content. Maybe you want to see Gruber roast Google Design. Maybe you want to see posts by Federico Viticci who recently returned to X. So I...
Original Article

Let me get you up to speed: Elon Musk took over Twitter and renamed it X and X became a shitty place and Nitter was an alternative X frontend that let you see X content without going to x.com and XCancel was one of the Nitter instances and as they got more popular they got cease-and-desist’d . This is why we can’t have nice things.

But sometimes people still post X content. Maybe you want to see Gruber roast Google Design . Maybe you want to see posts by Federico Viticci who recently returned to X. So I made Litterbox , a Safari extension that opens x.com links in a popup. The idea is you open it, take a look, gag a little, then close the lid.

A before-and-after pair: a Daring Fireball link to x.com circled in red, and the same post opened in a Litterbox popup.

Notice the little after the link. That’s Litterbox saying it can handle this one. Because sometimes people just link to their own profile page and you can’t embed profile pages. (I tried to decorate the link with stink lines, but some sites’ CSP blocks external images. *shakes fist* )

Litterbox doesn’t send your cookies when you open those popups, because of course. It uses the same API X uses for its website embeds. I don’t think they’ll kill the website embed feature. But if they do, it will be very funny.

Litterbox Privacy Policy

Litterbox doesn’t collect, store, or transmit any personal information.

Litterbox Pricing

Litterbox is available for free on the App Store . There are no subscriptions, in-app purchases, ads, or tracking. Requires iOS 18+ or macOS 15+.

If you want to support me, go buy my other apps. The expensive ones. Teleplayer . History Book . Subscribe to gibber.blog . My wallet will thank your wallet.

Litterbox Support

If you have any questions, feel free to contact me via email or Mastodon . I read all my emails and Mastodon mentions, but sometimes I’m too socially awkward to reply. Sorry about that in advance.

United Steelworker Talks Test Reach of Trump ‘Golden Share’ Deal

Portside
portside.org
2026-09-06 12:16:38
United Steelworker Talks Test Reach of Trump ‘Golden Share’ Deal Marti Sun, 09/06/2026 - 12:16 ...
Original Article

A novel national security agreement between US Steel and the Trump administration complicates labor relations for the company and competitor Cleveland-Cliffs as they race to negotiate new contracts with the United Steelworkers.

The USW’s original pact with both companies expired at midnight, but on Monday the parties agreed to extend the bargaining contract terms for thirty days while negotiations continue. The USW represents over 20,000 workers across both steel manufacturers.

The talks are an early test of the labor effects of President Donald Trump’s agreement that allowed him unprecedented control over US Steel in exchange for approving its acquisition by Nippon Steel Corp. Trump is now able to stop the combined company from relocating jobs overseas, closing or idling plants, or walking back planned investment decisions under most conditions.

The USW publicly opposed the acquisition and the “golden share” agreement last year, arguing that the Japanese steelmaker would transfer US Steel jobs to non-union facilities.

But labor experts said the deal could give the USW a boost in bargaining, particularly concerning job protections and other aspects of the company’s business strategy that US Steel already negotiated with the Trump administration.

“The golden share allows the union leverage over terms and conditions that are hard to bargain over,” said Alvin Velazquez , a labor and corporate law professor at Indiana University. “The real question is, how does it get used?”

Golden Share Effects

Parallel negotiations between the union and both companies began in Pittsburgh in July.

Because the framework gives the president a veto if US Steel wants to move jobs overseas, the union is already coming into the talks with a certain amount of job protections they would normally have to negotiate for, Velazquez said.

“It expands the possibility of what can be bargained for,” he said. “It’ll be a very interesting dynamic to see how that plays out.”

Trump’s deal with US Steel also gives the union added leverage in negotiations with Cleveland-Cliffs, even though it isn’t subject to the same presidential oversight, labor observers said.

Federal anti-trust laws typically exempt labor negotiations, so it’s not uncommon for unions to try and extract comparable concessions from companies that are competitors.

When one company settles, the other usually agrees to similar conditions to avoid a one-sided work stoppage. That could push Cleveland-Cliffs to accept some of the quasi job and investment protections US Steel already agreed to in the acquisition deal.

“Cleveland-Cliffs doesn’t want to have a strike. If Nippon/US Steel settles, that would put them in a very vulnerable position,” said Susan J. Schurman , a labor professor at Rutgers University.

The golden share, however, doesn’t prevent the company from moving jobs to non-union facilities, a chief concern for the USW when it was opposing the Nippon purchase. It also adds significant uncertainty for the workers whose jobs are now more subject to political whims.

“In the hands of a president who was consistently and soberly pro-union, it would be significant leverage,” Velazquez said. However, with a president “who doesn’t define themselves by their support of unions and union workers, it’s a wild card.”

The golden share creates hurdles for US Steel to adjust its investment strategies if economic conditions change, because many decisions are now subject to federal government review. That could make the company more hesitant to agree to higher wages and healthcare proposals, labor observers said.

The deal’s “constraints are operative and they’re significant,” said Lee Howard Adler , labor professor at Cornell University. “With that, the math has to be done extremely carefully and negotiations move more slowly.”

Union Priorities

This round of talks are an early test for newly elected USW President Roxanne Brown, who took office in March as the first woman and person of color to lead the 850,000-member union. Brown has touted the negotiations as a chance for the labor group to lock in guarantees for workers, and influence the company’s long-term strategy.

In an interview with Bloomberg News earlier this year, Brown said she would prioritize guarantees on employment levels and capital spending as well as healthcare and wages during the talks.

On July 23 US Steel offered the union a five-year contract which contained an 18% wage increase with no changes to the company’s current profit sharing and retirement plans.

The USW rejected that deal, saying the contract’s healthcare plans would shift major costs onto employees.

“USS hasn’t moved on their healthcare proposals that would almost certainly lead to substantial cost increases and benefits cuts, shifting costs and risk from USS to all of us,” local union leaders said in an Aug. 28 letter.

On the Cleveland-Cliffs side, the union also said the company “still has not proposed any wage increases yet” after five weeks of bargaining, and that similar to US Steel, it had presented a “healthcare plan that would mean paying monthly premiums and thousands more in deductibles while receiving less coverage,” according to bargaining updates .

“It looks to me like the healthcare issue is really the big one on the table. And I predict that the United Steelworkers aren’t going to agree to a contract where they have to have co-pays,” Schurman said.

The union said Monday that while the parties were still far apart, negotiations were expected to continue in the coming days and would prioritize “wages, benefits, job security, health and safety, capital investments.”

Amanda Malkowski, spokesperson for US Steel, said in an e-mail that the company was continuing to engage in “good faith negotiations” on issues concerning healthcare, wages, and capital investments. A spokesperson for Cleveland-Cliffs said the company wouldn’t comment on the negotiations out of respect for USW.

Research carried out using NetBSD

Hacker News
www.netbsd.org
2026-09-06 12:10:05
Comments...
Original Article

Several groups, organizations, and individuals have conducted research or performed demonstrations utilizing NetBSD as a vehicle. If you have such material that might be appropriate for this section, and would like to make it available here, please let us know!

Network orientated

Other


Network orientated

TCP in satellite networks (NASA Lewis)

NASA Lewis Research Center - Satellite Networks and Architectures Branch use NetBSD almost exclusively in their investigation of TCP for use in satellite networks. They are currently examining several proposed modifications to the Transmission Control Protocol (TCP) protocol. Extensions are being tested in an attempt to improve satellite communication. However, the extensions are also being tested in terrestrial environments. Among the extensions being tested are restransmission mechanisms based on selective acknowledgements (e.g., FACK TCP) and TCP with larger initial windows. They are currently testing the mechanisms' performance benefits and their fairness to other traffic. They are also working with the Internet Engineering Task Force (IETF)'s TCP Over Satellite and TCP Implementations Working Groups.

IPv6/IPsec (KAME)

KAME project . A research group for implementing IPv6, IPsec and other recent TCP/IP related technologies into BSD UNIX kernels, under BSD license. KAME stack got merged into NetBSD-current tree in June 1999.

Institute for Media Communication (IMK) Value-added Solutions

The Institute for Media Communication (IMK) Value-added Solutions department . The current set of projects carries out on NetBSD:

  • Network Access Control

    Implementing an access control language which is integretated into the ip stack and can be configured by the system administrator. This gives the system manager fine grained control over who is allowed to access the network, at what time, when logged in from a remote machine or not, etc...

    Certain users can be prohibited network access altogether ('nobody', 'bin', etc...). This reduces the risk of abuse on the machines.

  • Tunneling

    Setting up a tunneling server which allows users to tunnel insecure protocols (pop3, smtp, etc...) through a secure tunnel transparently.

  • Spam Protection

    They have developed a tool that protects Internet users from receiving unsolicited bulk mail (aka spam) and are currently evaluating whether it is a feasible solution.

  • Sendmail replacement

    They are developing a sendmail-replacement which allows an arbitrary number of simultaneous SMTP connects with only one process, using asynchronous i/o. First beta versions are -substantially- faster than sendmail, qmail or any of the other free MTAs.

TCP Vegas with Live Experiments

NetBSD TCP Vegas with Live Experiments . TCP Vegas is an extended slowstart TCP flow control from Lawrence Brakmo et al. at the University of Arizona. Prof. Peter B. Danzig and his group at usc.edu ported TCP Vegas to NetBSD 1.0.

NEC Europe Network Laboratories

NEC Europe Ltd. established the Network Laboratories in Heidelberg, Germany in 1997, as NEC's third research facility in Europe. Research and development functions are integrated into the same organization to shorten the time to market of cutting-edge network technologies. The laboratories place special emphasis on solutions meeting the needs of NEC Network's European customers.

The Heidelberg labs focus on software-oriented research and development for the next generation Internet. New communication architectures and protocols supporting multimedia and mobility over the Internet, together with intelligent Internet services, are the core of our work. A small market research team continuously analyses market trends and market requirements to assure that R&D activities address actual market needs.

NetBSD is used in the Network Laboratories for different task in the fields of research and IPv6 network operation among other free available BSD systems. The availability of NetBSD for different platforms makes it an ideal candidate for several activities, like:

  • running IPv6 routers and services on older computer systems, e.g. Sparc systems
  • testing research result in real life

The free available sources, the tight coupling of the sources to the well documented 4.4 BSD and the use of innovative techniques makes NetBSD an excellent software research platform.

NetBSD sets Internet2 Land Speed World Record

Researchers of the Swedish University Network ( SUNET ) have beaten the Internet2 Land Speed Record using two Dell 2650 machines with single 2GHz CPUs running NetBSD 2.0 Beta. SUNET has transferred around 840 GigaBytes of data in less than 30 minutes, using a single IPv4 TCP stream, between a host at the Luleå University of Technology and a host connected to a Sprint PoP in San Jose, CA, USA. The achieved speed was 69.073 Petabit-meters/second. According to the research team, NetBSD was chosen "due to the scalability of the TCP code" .

More information about this record including the NetBSD configuration can be found at: http://proj.sunet.se/LSR2/

The website of the Internet2 Land Speed Record (I2-LSR) competition is located at: http://lsr.internet2.edu/

A Scaleable Monitoring Platform for the Internet

SCAMPI is a two-and-a-half-year European project to develop a scaleable monitoring platform for the Internet. It also aims to promote the use of monitoring tools for improving services and technology.

The project develops a network adapter, initially at 10 Gbps speeds, tailored to the needs of monitoring tools. This includes development of an open and extensible monitoring architecture to support a secure and programmeable shared monitoring infrastructure. It will also investigate the technical challenges of developing monitoring systems for 100 Gbps speeds and beyond.

The Liberouter COMBO6 Card is considered to be adopted as a base of the SCAMPI hardware monitoring adapter for speeds up to 10 Gbps.


Other

The UVM Virtual Memory System

UVM is a new virtual memory system specifically designed to provide the I/O and IPC systems with a range of flexible data movement mechanisms. Implemented in the NetBSD operating system, UVM completely replaces the Mach based 4.4BSD VM system. In addition to featuring flexible data movement mechanisms, UVM also improves virtual memory performance over BSD VM in traditional areas such as forking and pageout. UVM is implemented entirely within the framework of BSD and thus maintains all the features and standard parts of the traditional Unix environment that programmers have come to expect.

Space Acceleration Measurement System II (SAMS-II)

SAMS-II Project - Space Acceleration Measurement System II

NASA will be measuring the microgravity environment on the International Space Station using a distributed system, consisting of:

  • RTS (Remote Triaxial Systems)

    Several PC104 ISA boxes running NetBSD 1.2.1 or 1.3.2.

  • ICU (Interium Control Unit

    An IBM ThinkPad 760XD as a temporary main box.

The first RTS was scheduled to be delivered in September 1998, for launch to the space station in Jan. 2000.

More information can be found in some mail to the port-i386 mailing list, which also has some more details on the hardware.

Massively-parallel And Real-time Storage

Project Massively-parallel And Real-time Storage (MARS)

Cluster based architectures that employ inexpensive Personal Computers (PCs) interconnected by high speed commodity interconnect have been recognized as a cost-effective way of building high performance scalable Multimedia-On-Demand (MOD) storage servers. Typically, the PCs in these architectures run operating systems such as UNIX that have traditionally been optimized for interactive computing and lack fast disk-to-network data paths and support for guaranteed CPU and storage access. In this work we report design, implementation and performance measurements of innovative enhancements to 4.4 BSD UNIX carried out to rectify these limitations in the context of our Massively-parallel And Real-time Storage (MARS) project. We have proposed and implemented the following enhancements to a 4.4 BSD compliant public domain NetBSD UNIX operating system:

  1. A new kernel buffer management system called Multimedia M-buf (mmbuf) which shortens the data path from a storage device to network interface,
  2. fair queuing within the SCSI driver for equitable resource sharing between real-time and non-real-time streams, and
  3. integration of these new OS services with a CPU scheduling mechanism called Real Time Upcall and a software disk striping driver called Concatenated Disk (ccd).

Our experimental results demonstrate that these enhancements provide throughput improvements and QOS guarantees on the data path from the disk to network.

There is a paper describing the OS enhancements that appeared in IEEE Multimedia98, and there are several papers on the "Project MARS-Scalable, Web Based Multimedia-On-Demand Servers and Services" of which this work was a part:

http://www.ccrc.wustl.edu/~milind/MediaServers.html

New Callout and Timer Facilities

Research carried out at the Washington University in St. Louis: Current BSD kernels (4.4BSD-Lite and derivatives) take time proportional to the number of outstanding timers (``callouts'') to set or cancel timers. This implementation takes constant time to start, stop, and maintain timers leading to a highly scalable design that can support thousands of outstanding timers without much overhead. Unlike the existing implementation, it is guaranteed to lock out interrupts only for a small, bounded amount of time.

Group Awareness in Distributed Software Development

Open-source software development projects are almost always collaborative and distributed. Despite the difficulties imposed by distance, these projects have managed to produce large, complex, and successful systems. However, there is still little known about how open-source teams manage their collaboration. In this paper we look at one aspect of this issue: how distributed developers maintain group awareness. This paper aims at analyzing various Open Source projects (NetBSD, Apache, Subversion) and analyzing group structure and communication.

Using Application-Driven Checkpointing Logic for Hot Spare High Availability

Antti Kantee is investigating the use of application-driven checkpointing logic for hot spare high availability in this research work , which was in part presented at EuroBSDCon 2004 .

Cultivating Trust

Hacker News
kaeruct.github.io
2026-09-06 12:02:37
Comments...
Original Article

Part of “ Conquering Entropy

Most of the issues I have with AI-generated code are related to trust. Do I trust the person who wrote this ticket? Do I trust that the engineer who opened this PR understood the ticket and guided the coding agent to implement it properly? Do I trust the coding agent’s implementation? Do I trust our test suite to catch regressions before they hit production? Do I trust our CI/CD to properly build, test, and deploy our change? Do I trust our observability setup to alert us when the ai-generated code breaks production? Do I trust the AI SRE (Site Reliability Engineer) to properly diagnose the issue and help us mitigate it? Do I trust GitHub not to have an incident when we need it the most?

Trust is hard to earn and easy to lose. So I think it’s crucial to foster a culture of trust within the engineering team. You must trust engineers to do the right thing.

Encouraging Accountability

I find it helpful to clearly communicate something like this: “You are accountable for what you ship. If this breaks production and you authored the PR, you should be there to fix it.”

If engineers are made accountable for the code they ship, they should be given the agency to produce it with their preferred methods. Even if you’re not AI-pilled, you have to admit that AI agents do generate a lot of code very fast. Code still needs to be generated, and the expectation is that now it’s cheap to generate a lot of it. To cope with this, engineers must be allowed to put some measures in place to ensure the quality of the codebase does not degrade.

In a healthy organization, the engineers should trust each other to only push code of reasonable quality. I say reasonable because it’s not pragmatic to obsess over quality and try to ship always 100% perfect code. Even before coding agents most code was already a buggy mess! So it’s understandable when a “good enough” solution is delivered. Often, we trade speed of delivery for quality, and incur some technical debt.

Here are some practices I have found useful to facilitate engineering accountability in this new era of coding:

Coding Guidelines

Have a clear technology strategy: take some time to decide what matters for your codebase and invest in clear guidelines. Both for humans and for the coding agents. Even if the humans don’t read the guidelines, their coding agents will, and will follow them (mostly).

Deterministic Tooling

Make heavy use of deterministic tooling to ensure code quality. Typed languages, linters, dead code detection, security scans, CI/CD, and so on. All of these tools existed before coding agents and help us in the fight against slop. (Stay tuned for a follow-up post with specific recommendations!)

Enforce Small PRs

Empower the engineers to reject unreviewable PRs. If possible, codify this criteria so any unreviewable PRs are immediately rejected. Of course, make sure to leave room for exceptions.

Own the Tests

Write test cases by hand. This is similar to the job of a business analyst. Deeply think about the feature and define proper test scenarios. Discuss this within the team. Coding agents can implement the tests, but they should be defined by humans.

Taste in Product

Work in tandem with product to have a coherent product vision. It’s very easy to go crazy with AI and implement whatever feature comes to mind. Make sure you only implement useful features that actually bring value to users!

Prototype

Throwaway code. Since code is very easy to generate now, it’s a good opportunity to try different approaches. Don’t just let the coding agent generate one solution. For example, try three radically different approaches and pick the one that best fits the problem and existing system.

Focus on the Outcome

Be pragmatic about the result. Sometimes the code is not the result. The result is a report, or a tool that helps you accomplish something else. For cases like this the quality of the code is not that relevant as long as the result is useful.

NetBSD 9.5 released and EOL for netbsd-9

Lobsters
blog.netbsd.org
2026-09-06 11:44:40
Comments...
Original Article

NetBSD 9.5 released and EOL for netbsd-9

September 06, 2026 posted by Martin Husemann

The NetBSD Project is pleased to announce NetBSD 9.5, the fifth (and final) release from the NetBSD 9 stable branch.

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

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

Full release notes, including download links

[ 0 comments ]

Alberta (Canada) slaps punitive tax on solar panels

Hacker News
albertapolitics.ca
2026-09-06 11:14:06
Comments...
Original Article
Solar panels – an increasingly common sight in Alberta, one that the United Conservative Party would like to make a lot less common (Photo: ArkRenewable.ca).

Never let it be said that the Alberta’s United Conservative Party government isn’t in favour of recycling stuff – at least as long as that can be used to create higher costs and more red tape to help strangle the development of renewable energy.

Alberta Premier Danielle Smith, still loyally treading the Trump path on renewable energy (Photo: Alberta Government/Flickr).

Yesterday, the UCP opened a new front in its War on Renewable Energy by adding a $14-per-panel “recycling fee” to the cost of solar panels.

Naturally, the government framed the effort as an example of environmental responsibility, claiming piously in a news release that “Alberta’s government will not allow solar panels to pile up in landfills across the province.”

With the new fee set to come into effect quickly, on Oct. 1, the province will also ban solar panels from landfills and “ensure they are directed toward reuse and recycling when they reach the end of their useful lives.”

If we could trust the UCP on environmental initiatives, this might be a convincing pitch, but the record of Premier Danielle Smith and her government on this front is not reassuring.

The Business Renewables Centre-Canada condemned the “punitive tax” as “the latest in a string of arbitrary costs the province has placed on the renewable energy industry without the analysis to back it up.”

Jorden Dye, director of the Business Renewables Centre-Canada (Photo: BRC-Canada).

“While BRC supports the introduction of a recycling fee, we cannot support a punitive tax far exceeding any other jurisdiction,” said the organization’s director, Jorden Dye, in a news release yesterday.

“While governments worldwide are beginning to establish end-of-life recycling programs for clean energy infrastructure, Alberta’s proposed fee is 139 to 3500 per cent higher than any other jurisdiction in the world,” the release said. Well, of course it is! This is the UCP we’re talking about.

BRC-Canada  – which was set up by the Calgary-based Pembina Institute to help businesses access renewable energy to reduce emissions, pretty much making it Public Enemy No. 1 in the eyes of the UCP – called the imposition of the fee part of a familiar pattern. It cited its June 2025 study that found costs higher than in any other jurisdiction we reviewed, “imposed without benchmarking to justify them.”

According to the government, “by 2045, more than 95 per cent of the solar panels currently installed in Alberta are expected to reach the end of their lives, generating as much as 72,700 tonnes of material.”

Alberta Environment Minister Grant Hunter (Photo: David J. Climenhaga).

But according to BRC-Canada, since none of the panels sold now are likely to require recycling for 25 to 35 years, “no near-term waste stream justifies the urgency, and the Alberta Recycling Management Authority has not published a breakdown explaining how the $14 figure was calculated.”

Never mind, the UCP had a colourful canned quote for Environment Minister Grant Hunter: “We will not wait until mountains of dead solar panels are piling up in our landfills before acting. We are putting the system in place now to recover valuable materials, attract private investment and build a new recycling industry here in Alberta.”

An image of a stack of dented solar panels was included to lend verisimilitude to the claim.

“Responsible solar energy growth means considering environmental responsibility throughout its life cycle, said RJ Sigurdson, whose unintentionally ironic title is minister of affordability and utilities, in the same government news release. “We’re protecting taxpayers from future cleanup costs and keeping power affordable and sustainable for generations to come.”

Alberta Utilities and Affordability Minister RJ Sigurdson (Photo: Alberta Government/Flickr).

In other words, I guess, the UCP is not only picking winners, it’s trying to ensure an industry that has the potential to cut into oil and gas profits is a loser.

Well, this is the Trump path, which Premier Smith makes an obvious effort to follow closely even as she spars with critics of the U.S. president’s anti-Canadian trade policies.

So yesterday’s announcement was obviously on brand considering the same government’s 2023 ban on renewable energy projects and subsequent imposition of expensive and complicated red tape on renewable energy projects that was estimated to have driven as much as $33 billion in renewable projects out of the province – although that figure is naturally disputed by the government.

Just in case you were thinking solar panels might be a good way to insulate yourself and your family from the additional $460 a year in electricity costs you can expect to result from the government’s sweetheart deal with Meta Platforms Inc., they’re going to cost you a little more now.

That Meta deal will let the California-based corporation hook into the provincial power grid for a couple of years to run its planned energy-sucking hyperscale AI data centre northeast of Edmonton before a natural-gas fired power-generation plant is brought on line, if it ever is.

That is widely forecast to drive up electricity costs for almost everyone in Alberta.

As has been observed in this space before , nobody loves red tape as much as Premier Smith and the UCP, especially if it can be weaponized against the government’s enemies, real and imagined, and made to help its friends.

Research acceleration: The view inside OpenAI

Hacker News
openai.com
2026-09-06 11:08:44
Comments...

I Feel about AI

Hacker News
beza1e1.tuxen.de
2026-09-06 11:00:19
Comments...
Original Article

I feel surprise at how well neural networks work. LLMs work like "gut feeling" as they just generate one more token after another. There is no planning or reasoning algorithm inside. Yet, some planning and reasoning emerges.

I feel fear about an impending doom. Yudkowsky's argument that a superintelligent AI will inevitably destroy humanity seems to have no flaw. Yet, nobody seriously tries to sandbox AIs because they are too useful with access.

I feel disgust about AI companies destroying the open web society. Their crawlers and agents make it increasingly difficult to sustain an open community on the web. Like a locust plague, they descend on any open wiki and forum and overwhelm them.

I feel sad about the artists who suffer due to AI-slop competition. Being an artist always required some sacrifice and only a few are fortunate to gain some wealth. This is even harder now that AI can generate some kinds of "content" cheaper and faster.

I feel anger about the inability of our political systems to rein in the power of capital. While I can rant about politicians a lot, I still prefer a democratic government to have power instead of billionaires. It essentially just comes down to making some decisions in a coordinated way, but most people are not even aware of the problem. This also covers the environmental destruction by AI companies as they greedily gobble up all kinds of resources.

I feel happy to live in this age of discovery. We might observe the creation of artificial intelligence. Working in software development, the practice was radically transformed forever in 2026. It is fascinating to observe this firsthand. It allows me to generate stuff (code, images, music) which would never have been created otherwise.

As a conclusion, does the good or the bad outweigh the other? It seems positive on a technological level to me, but bleak on a societal level.


(I used Mistral as reviewer here. I typed every word myself.)

It's complicated

Recreating Minecraft Is Not a Benchmark

Hacker News
kuber.studio
2026-09-06 10:52:07
Comments...
Original Article

GPT Astra released a couple of days ago and, inevitably, within the hour my entire feed was the same five things: recreating Minecraft in one prompt, painting themselves in MS Paint, the pelican riding a bicycle as an SVG, a ball bouncing in a rotating box with believable gravity, and an SVG game controller.

On paper these look like harder, more visual problems for a model to solve, there’s a reason they’re as big as they are. I’ve started calling them demo-benchmarks, visual and understandable enough for everyone to get but finite enough for the next model to be “perfect” on.

That’s the problem, these tests can’t tell you how good a model is anymore because it’s trivial for labs to optimise for exactly these tests by the next release.

It’s not really their fault either, honestly I’d say it’s dumb if they didn’t - nothing sells a launch like a pelican or a 3D game controller the timeline can’t stop quoting.

View Tweet

A fixed, famous target and eight weeks of runway is a solved pelican, these tests never change and anything that never changes can be overfit. Every launch cycle proves it again.

A test you can perfect on a schedule measures preparation instead of capability, to me that’s anti the very definition of a benchmark, it should be a hard test, something very hard to perfect.

Claude Fable 5 recreating Minecraft in one prompt

The same dynamic runs through the open evals, smaller models that feel dumber in practice still outscore better ones on sites like Artificial Analysis. This isn’t hypothetical, Thinking Machines’ Inkling Small scored within a point of its flagship sibling on the Artificial Analysis Intelligence Index with less than a third of the parameters and beat it on Humanity’s Last Exam, GPQA Diamond and SciCode. Public, static, famous test sets leak into training data and fine-tuning choices.

Inkling Small, a third of the size, matching its flagship on the Artificial Analysis Intelligence Index

A launch is a first impression and first impressions are marketing, that’s why you’re always bound to be shocked - the shock was scheduled.

So what’s the alternative? Honestly, I’m not sure

because if you think about it, the obvious fix somewhat already exists. LiveBench rotates its questions, ARC-AGI keeps a private set, Humanity’s Last Exam holds part of itself back. Tests where the tested party doesn’t know what’s being tested: you can’t teach to a test that hasn’t been written yet.

But if holdout evals are the answer, though, why does the pelican still win?

The answer is because most of social media doesn’t need to understand a research paper to notice that the bicycle finally has pedals or is animated. A demo benchmark makes it obvious why it’s a better capability in seconds.

So no, there is no alternative to a good demo. But there are better alternatives to demo-benchmarks for seeing how good a model actually is. And if a model scores well but keeps failing at your work, that is actually a gap that deserves investigation.

Demo-benchmarks make great content. I just wish we’d stop grading with them.

The purpose of DNS is to spread scams

Simon Willison
simonwillison.net
2026-09-06 10:40:07
The purpose of DNS is to spread scams Terence Eden shares some daunting statistics in support of his take that "the Domain Name System's purpose seems to be a vector for criminals to run scams on people at a terrifyingly high rate". On this Interisle report (via Andrew Campling), Terence says: It s...
Original Article

6th September 2026 - Link Blog

The purpose of DNS is to spread scams . Terence Eden shares some daunting statistics in support of his take that "the Domain Name System's purpose seems to be a vector for criminals to run scams on people at a terrifyingly high rate".

On this Interisle report ( via Andrew Campling ), Terence says:

It says 85 million new registrations of gTLDs were made in 2025. Of those 8.5 million were added to blocklists by May 2025. It reckons that a 10% abuse rate is the likely floor for these numbers and it's probably closer to 20%. One in five newly registered domains with a gTLD are scams. That's a bloody crisis.

I had no idea. Apparently ICANN have been discussing this problem for years.

Show HN: Kadō – open-source habit tracker, with non-binary habit score, for iOS

Hacker News
github.com
2026-09-06 10:34:46
Comments...
Original Article

Kadō

A privacy-first habit tracker for iPhone and iPad.

Non-binary habit score · Offline-first · Open source (MIT) · No account, no subscription, no telemetry.

Download on the App Store

getkado.app

MIT license App Store 1.7 Swift 5.10 Platforms


What is Kadō?

Kadō is an iOS habit tracker with three commitments:

  1. A score, not a streak. The habit score — inspired by Loop Habit Tracker on Android — is a non-binary exponential moving average of your completions. One missed day doesn't wipe your progress. You see a trend, not a fragile chain.
  2. Your data stays yours. No account, no analytics, no third-party SDKs. Storage is local (SwiftData), sync is optional through your own iCloud (CloudKit private database). Export is lossless, in JSON or CSV, whenever you want it. See PRIVACY.md .
  3. Native to Apple. SwiftUI, @Observable , SwiftData, CloudKit, WidgetKit, App Intents — no cross-platform layer, no third-party dependencies.

Why another habit tracker? The market gap is spelled out in docs/PRODUCT.md : the reference open-source tracker (Loop) is Android-only; the reference iOS tracker (Streaks) is closed source and uses a binary streak; the most visible modern iOS open source attempt (Teymia) is freemium. Kadō takes Loop's algorithm to native iOS — MIT, free, no account, no subscription.

Screenshots

Today view Habit detail with score popover Multi-habit overview New habit form Today view in dark mode

Features

  • Today view — habits due today, tap to complete, long-press for partial / note / timer, drag to reorder (the order syncs via iCloud).
  • Habit detail — monthly calendar with month-by-month navigation, current streak, best streak, habit score with an info popover explaining the math. Edit any past day straight from the calendar, including days before the habit existed.
  • Per-day notes — a short note on any day's completion, carried through export and import.
  • Overview — habits × days matrix with score-shaded cells, the Loop / Way of Life pattern with Kadō's score DNA. Completions logged off-schedule show up too, instead of hiding as rest days.
  • Flexible schedules — daily, N days per week, specific weekdays, every N days (the cycle re-anchors on each completion, so finishing early never costs you a day). Binary, counter, or timer habit types.
  • Day starts at — push the day rollover as late as 6 AM, so late-night logging still lands on the day you mean. Changing it never re-buckets history.
  • Widgets — Home Screen (small / medium / large) and Lock Screen (rectangular / circular / inline). Quick-complete via AppIntent .
  • Siri and Shortcuts — complete a habit, log a value, or ask for a score and streak, hands-free. Also available as Home Screen actions and Shortcuts automations.
  • Reminders — per-habit local notifications with recurring schedules and check / skip quick actions.
  • iCloud sync — optional, opt-in, goes only through the user's private CloudKit database. Settings shows live sync state, including when sync isn't working.
  • JSON and CSV export / import — lossless backup of your data, in both formats, round-trip tested. Open a CSV backup in Numbers or Excel, edit it, bring it back in.
  • Tip Jar — entirely optional, unlocks nothing. The whole app stays free, with no ads, no subscription, and no tracking.
  • Accessibility — Dynamic Type up to XXXL, VoiceOver labels on every surface, full Dark Mode.
  • Localization — English and native French (not machine-translated).

Status

Shipping on the App Store — current version 1.7 .

Version Highlights
1.0 First public release — score, Today / Overview / Detail, widgets, iCloud sync, reminders, JSON export, EN + FR
1.1 Siri and Shortcuts; edit past days from the calendar
1.2 Per-day notes; backdated completions; month navigation
1.3 Drag to reorder habits; a gentle, one-time review prompt
1.4 iCloud sync reliability — iPad crash fix, honest sync status
1.5 Tip Jar (StoreKit 2), optional and unlocking nothing
1.6 "Day starts at" hour; days-per-week scoring fix; off-schedule completions in Overview
1.7 CSV export / import round-trip; "every N days" re-anchors on completion

Not planned. A native Apple Watch app and HealthKit auto-completion were scoped for v0.3 and have been descoped — no user has asked for either since launch. Both stay listed in docs/ROADMAP.md under "Descoped", and would be reconsidered on real demand.

Next — Live Activities and Dynamic Island for timer habits, then the remaining v1.x polish (themes, biometrics, categories, backup files). Full roadmap in docs/ROADMAP.md .

Tech stack

  • SwiftUI with @Observable (iOS 17+) for state — no Combine.
  • SwiftData for local persistence, with VersionedSchema + SchemaMigrationPlan wired from day one.
  • CloudKit via SwiftData ( cloudKitDatabase: .private(...) ) for multi-device sync.
  • WidgetKit + an App Group JSON snapshot for extension surfaces (the widget process never opens SwiftData — see CLAUDE.md for why).
  • App Intents for widget quick-complete, Siri, and Shortcuts.
  • StoreKit 2 for the Tip Jar.
  • Swift Testing for unit tests, XCTest for UI tests and for the App Store screenshot run.
  • Zero third-party dependencies.

Target: iOS 18.0+, Xcode 16.0+, Swift 5.10+.

Architecture notes, conventions, and toolchain quirks (SwiftData edge cases, CloudKit-shape rules, concurrency under Swift 6, etc.) are documented in CLAUDE.md .

Repository layout

Kado/                       # Main iOS app target
Packages/KadoCore/          # Shared Swift package — @Model types,
                            #   domain types, calculators, intents,
                            #   widget snapshot types
KadoWidgets/                # Widget extension target (reads an
                            #   App Group JSON snapshot)
KadoTests/                  # Unit tests (Swift Testing)
KadoUITests/                # UI tests (XCTest) + the App Store
                            #   screenshot run
Scripts/                    # Screenshot capture, framing, and the
                            #   dependency-free App Store Connect client
site/                       # getkado.app — static marketing site
branding/                   # SVG marks and wordmarks
docs/
├── PRODUCT.md              # Product vision, competitive analysis
├── ROADMAP.md              # Versioned feature roadmap
├── habit-score.md          # Score algorithm spec
├── streak.md               # Streak algorithm spec
├── app-store/              # Listing copy, captures, framed images,
│                           #   and how they are pushed
├── app-store-connect.md    # Store metadata, copy, checklists
├── plans/                  # Per-feature research / plan / compound
│                           #   artifacts from the conductor workflow
└── screenshots/            # iPhone 6.7" set (EN + FR), also the
                            #   source for the marketing site
PRIVACY.md                  # Privacy policy (repo-hosted)

Development

Requirements

  • macOS 14.5+
  • Xcode 16.x+
  • An Apple Developer account is only needed to build to a physical device or TestFlight; the simulator does not require one.

Build and run

git clone https://github.com/scastiel/kado.git
cd kado
open Kado.xcodeproj

Select the Kado scheme and an iOS 18 simulator (iPhone 17 Pro is the project's practical default on Xcode 26 toolchains).

Tests

From Xcode: ⌘U on the Kado scheme.

From the command line, the Makefile wraps the common loops:

make test    # unit suite (Swift Testing) — a couple of seconds
make e2e     # UI suite (XCUITest), minus the screenshot run
make build   # compile the app for the simulator

make help lists the rest, including the App Store targets ( make screenshots , make frames , make listing ) documented in docs/app-store/README.md .

If you use Claude Code with the XcodeBuildMCP server, prefer test_sim / build_sim over shell xcodebuild — see the "Tooling" section of CLAUDE.md .

Dev mode

Settings has a hidden dev-mode toggle that swaps the SwiftData container for a seeded local sandbox. Useful for playing with historical score / streak behavior without affecting your real data. See docs/plans/2026-04/dev-mode/ for the design notes.

Contributing

Issues and pull requests are welcome. A few notes:

  • Read CLAUDE.md first — it is the project's working agreement (architecture, conventions, testing expectations, toolchain quirks). It is written for Claude Code but applies equally to human contributors.
  • Business logic (score, streak, frequency, import/export) is test-first. New behavior needs a test; regressions need a test that would have caught them.
  • One PR per feature or logical fix. Commit message format follows a lightweight Conventional Commits convention — feat(scope): description , fix(scope): … , etc.
  • No third-party dependencies. The Tip Jar is built directly on StoreKit 2, and the App Store Connect client in Scripts/ signs its own JWT with openssl , precisely so that stays true.

Kadō collects nothing. No analytics, no telemetry, no crash reporter. iCloud sync is optional and goes through the user's own private CloudKit database.

Full policy in PRIVACY.md .

License

MIT © 2026 Sébastien Castiel.

Acknowledgements

  • Loop Habit Tracker (Álinson S. Xavier, GPLv3) — the source of the non-binary habit score algorithm. Kadō reimplements the idea natively in Swift; no Loop code was copied.
  • Streaks (Crunchy Bagel) — the iOS UX reference for how this kind of app should feel.
  • Teymia Habit (MIT) — reference point for a modern SwiftUI + SwiftData + CloudKit + WidgetKit stack on iOS.
  • XcodeBuildMCP (getsentry, MIT) — the MCP server that makes an autonomous build / test / screenshot loop possible with Claude Code.

A/I shuts down – Stay human

Hacker News
keepitfree.ai
2026-09-06 10:34:34
Comments...
Original Article

The day we discovered we had been designated a global terrorist organization, we promised that we would try to resist as long as this was possible, that we would not back down as long as we could see options to stand our ground. For years, we have proudly maintained and defended a free, privacy-friendly, autonomous and politically committed infrastructure.

“Stay human” is not an empty phrase to us.

It means, above all, that we have a responsibility to protect our users, those who build the human networks we rely, and for the communities who flooded us with messages of support and solidarity.

We cannot engage in a fight or expose ourselves to manipulations that threaten the lives of these people and their loved ones, leaving them at the mercy of autocrats, fascists and agencies that take extra-legal courses of action. The possibility that our work may cause legal and financial consequences to those who are close to us - or even only have something to do with us - leaves us no choice.

A/I is shutting down. The Autistici/Inventati collective is shutting down and will soon discontinue all of the services we provide.

There is no easy way to announce or explain this

Every day we stayed online after August 26, 2026, has been a victory, but now we are forced to stop.

None of us holds eroic gestures and martyrs in high esteem, therefore we will demand no sacrifices. Not from us, not from anyone else. In this political climate, continuing to offer our services endangers our users and anyone who is part of our communities. In a world where allegations are disconnected from reality, we can only expect repression to be increasingly disproportionate. Under these circumstances, we are no longer able to maintain our original mission - offering secure and non-commercial digital tools.

These 25 years have been amazing. It’s been an “incredible ride”.

Now turn off your PCs, get out of your homes, struggle, hug each other and keep on smiling. We are stopping, but the Resistance and ideas do not stop. Stay human.

We will soon send instructions on how to back up the content of your blogs, mailboxes and websites, along with more technical recommendations. But keep in mind that, just as the autistici.org domain was made unreachable without previous notice, in the next few days we might suffer similar issues that we may not be able to predict.

Autistici/Inventati Collective

"Simple Made Easy" (2011)

Lobsters
www.youtube.com
2026-09-06 10:25:59
Every once in a while I come across this video again, and I always end up watching the whole thing. I'm sad I was never able to make it to a Strange Loop event. Comments...

Attackers conceal phishing lures using invisible Unicode characters

Bleeping Computer
www.bleepingcomputer.com
2026-09-06 10:23:46
Threat actors have adopted the ASCII smuggling technique in phishing campaigns, using invisible Unicode characters to evade email security filters. [...]...
Original Article

Attackers conceal phishing lures using invisible Unicode characters

Threat actors have adopted the ASCII smuggling technique in phishing campaigns, using invisible Unicode characters to evade email security filters.

ASCII smuggling has been used in AI prompt injection attacks to conceal malicious instructions from users by encoding them with Unicode characters from the Tags block (U+E0000–U+E007F).

Microsoft threat researchers discovered a large-scale phishing campaign using this technique, which peaked at up to 2.37 million daily messages in late February. Although the volume has dropped gradually in May, the operation is still active.

“The high-volume phase persisted for roughly three months after February 9 and dropped sharply after May 15, 2026,” explains Microsoft .

“These dates bound the observed use of the specific technique in our telemetry, not the broader campaign, which started earlier without it and continued without it.”

Phishing email delivery volumes
Phishing email delivery volumes
Source: Microsoft

In this campaign, the attacker inserts an invisible Unicode character inside finance-related lure words to split them.

In doing so, a keyword like ‘funding’ becomes something like ‘ fun[invisible character]ding ’ and evades email filters that rely on word lists to detect suspicious or malicious messages.

Sample of a phishing message
Sample of a phishing message
Source: Microsoft

Microsoft says the method has been used in millions of finance-themed phishing messages and works as intended, although Defender still caught over 99% of the messages based on other signals (sender, IP, domain, reputation checks).

On February 9, Microsoft identified a cluster of 148 finance-themed sender domains powering this campaign, accounting for about 96% of all messages its new Defender for Office 365 hunting logic flagged for Unicode-tag signatures.

Unicode characters in the text
Unicode characters in the email
Source: Microsoft

The domains used words such as “funding,” “capital,” “loan,” “advance,” and “credit,” and the messages promoted business funding, loans, and credit services.

The messages were delivered through infrastructure associated with the legitimate ActiveCampaign email-marketing platform.

After receiving Microsoft’s report of service abuse, ActiveCampaign said its moderation systems detect invisible Unicode characters the same way they detect unobfuscated text and treat heavy use as suspicious.

Microsoft recommends that defenders strip or normalize Unicode tag characters and other invisible code points before applying keyword, regex, or signature-based detection, and treat unexpected tag-block characters as a strong anomaly.

Applying the same normalization before passing email content to AI assistants should mitigate the risk of prompt-injection attacks.

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

Asahi Linux Now Officially Supports Apple M3 Macs – With Caveats

Hacker News
www.phoronix.com
2026-09-06 10:08:59
Comments...
Original Article

APPLE

Asahi Linux developers announced today that they are now officially supporting the Linux distribution on Apple M3 powered Macs. With the exception of the Mac Studio M3 Ultra, other M3 / M3 Pro / M3 Max devices should now work with the latest Asahi Linux builds but with some feature caveats.

The Asahi Linux developers announced today that the Apple Macs with M3 series SoCs have reached a state now where "almost" everything is supported to a state similar to the older M1 and M2 hardware.

M3 Macs

The biggest exception though is the GPU support, which they acknowledge is not yet performant or power efficient for 3D acceleration. They hope to have more GPU progress in the coming months.

In addition to the poor GPU support, Asahi Linux on the Apple M3 also does not currently provide sleep support due to the lack of DCP support. Without DCP support, the HDMI port on M3 MacBooks is also not working. This M3 support should cover all Apple devices with the exception of the Mac Studio M3 Ultra.

More details on this official support now for the downstream Asahi Linux on Apple M3 Macs can be found via AsahiLinux.org . The upstreaming of all the M3 support remains ongoing. There is the initial M3 support in the mainline Linux kernel for basic boot support but various other patches remain for upstreaming in Linux 7.4+ before it will reach a state similar to Asahi Linux itself.

The Ironies of Automation (1983)

Lobsters
static1.squarespace.com
2026-09-06 09:40:36
Comments...
Original Article
No preview for link for known binary extension (.pdf), Link: https://static1.squarespace.com/static/644321e78cd2dd37613af33e/t/6694873f71612132a84371c7/1721009983702/Ironies+of+Automation_Bainbridge_1983.pdf.

The many mysteries and lessons of the Bayeux tapestry

Hacker News
economist.com
2026-09-06 09:38:07
Comments...

NetBSD 11 from scratch

Lobsters
meanmicio.org
2026-09-06 09:35:03
Comments...
Original Article

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

This post is by no means a replacement for the NetBSD official guide . Sysinst is the the facto installer for NetBSD and is the most documented, straightforward way to install the operating system. This post is about having fun and understanding and learning the internals of the operating system. It also offers the highest flexibility to customize and overcome issues that may arise during a conventional installation.

I will try to establish a chronological order in the process, documenting the most relevant steps with screenshots and references to sources related to the topic.

MBR installation: NetBSD is notorious for its portability and being able to work in “old” computers. I decided to do the installation in a HP Pavillion (around year 2012) that uses MBR instead of the GPT partitioning found in more modern UEFI systems. (quick digression: Manyy of the so called “obsolete” or “old” computers are still useful and perform very well. You just need a good operating system ;). By adopting them, you will giving refurbished computers a great second life, you will be saving a lot of money and, most importantly, you will be preserving the environment from polluting waste.).

Let’s start 🙂

The installation image

The first step to install NetBSD is to download and copy the image to the USB pen drive.

Note: <usb_device> is the assigned device of your USB drive . Use the entire disk (ej sdb) instead of a partition. Please also note that this operation will wipe out your entire flash drive . Triple check that you are actually using the right device and not another disk of your computer.

Note: I will use the # as the shell prompt to denote root operations while the $ is for regular users.

Once it finishes, you can plug it in on your USB socket and boot the computer. Make sure you select the device as the first bootable device in your BIOS.

Preparing the Installation

Exit “sysinst”

When you boot from the usb flash drive, you will get at some point the sysinst installation program. Press CTRL+C to exit from sysinst and get a prompt.

You will see the following message followed by the root prompt.

Sysinst terminated.
To return to the installer, quit this shell by typing 'exit' of ^D
#

Setup the keyboard layout

If you have a non English keyboard, you might want to set the keyboard layout with the following command. For instance, to enable the Spanish keyboard layout, type the following command:

# wsconsctl -w encoding=es
encoding -> es

Note: The NetBSD console only admit ASCII characters, so letters like “Ñ” won’t be shown, but the layout will be correct.

Setup networking

To make the installation process more comfortable, I decided to enable networking and the SSH server in the target computer, so I can also take screenshots and document it better.

I will be using a LAN, so connect your ethernet cable to the router and enable DHCP. The following command will retrieve the IP and default gateway from your router DHCP server.

# /sbin/dhcpcd -d -n re0

“re0” is the network interface driver. NetBSD uses the driver name instead of the GNU/Linux “eth” nomenclature. In my case, “re0” stands for Realtek ethernet driver.

You can check the name of the interface and its status using the ifconfig command

Output from the ifconfig command, showing the interfaces (re0 and lo0) details.

Enable SSH server

Add a local user . It’s a good practice to create a local user for non-privileged operations, as well as to remotely login without using “root”. We will create the user “malatesta” and make him a member of the “wheel” group, so he can do a “su -” operation and assign a password.

Note: This user only lives in the installation usb drive.

# useradd -m malatesta
# usermod -G wheel malatesta
# passwd malatesta

Next we need to start the SSH server, so the live system will allow remote connections and we can resume the installation from another computer.

# service sshd onestart

Setting up disks and partitions

Now we are ready to start configuring important, non-volatile resources, starting with the target disk that will hold the operating system.

About Disks, partitions and BSD disklabels

Before we move on, I think is pertinent to talk a bit about how NetBSD deals with disks and partitions.

Disks

In NetBSD, and similar to the network interfaces, physical disks are represented by the driver interface, followed by an integer. The logical and raw devices are under the “/dev” directory. The following are some of the drivers:

wd: IDE drives (atabus). Regular hard drives
sd: SCSI drives (scsibus). This includes USB pendrives.
cd: CDROM drives

To list the current disks in your computer, use systcl hw.disknames command:

# sysctl hw.disknames
hw.disknames = wd0 cd0 sd0 dk0 dk1

In this example, wd0 is the IDE drive (where we’ll be installing NetBSD), cd0 is the CDROM, sd0 the USB pendrive and finally dk0 and dk1 are “wedges” (GPT partitions) belonging to sd0.

Now we know -from the naming conventions- that wd0 is our internal hard drive, and that is where we will install NetBSD.

Partitions

Diagram showing the NetBSD disk partitioning schema, both the MBR and disklabels. Source: NetBSD guide (https://netbsd.org/docs/guide/en/)
Diagram showing the NetBSD disk partitioning schema, both the MBR and disklabels. Source: NetBSD guide ( https://netbsd.org/docs/guide/en/ )

As previously mentioned, we will use a non UEFI, BIOS computer, so we’ll be installing OpenBSD using the MBR (Master Boot Record) partitioning schema.

  • Master Boot Record (MBR) . Traditional BIOS had a way of identifying the different partitions on a disk by writing their attributes in the first sector of the physical disk. The MBR partitioning schema limits the number of physical partitions (also called slices” in NetBSD jargon) to 4, and deals with disks of 2 terabytes (TB) or less. It is managed with the fdisk program .
  • Disklabel : The disklabel is a NetBSD feature that allows the creation of multiple partitions within an MBR slice. The disklabel information is stored in the MBR, and is managed with the disklabel program .

Partitioning the hard drive to install NetBSD

We already know what will be our target hard drive (wd0 in this particular instance). In order to install and make it bootable, we need to partition it. We will use the entire hard drive for NetBSD, although you could install more than one operating system in the same physical device.

Showing the current partitions

To list the details of the partition table, run the command fdisk along the disk drive (e.g. fdisk wd0)

# fdisk wd0
# fdisk wd0
Disk: /dev/rwd0
NetBSD disklabel disk geometry:
cylinders: 1938021, heads: 16, sectors/track: 63 (1008 sectors/cylinder)
total sectors: 1953525168, bytes/sector: 512

BIOS disk geometry:
cylinders: 1023, heads: 255, sectors/track: 63 (16065 sectors/cylinder)
total sectors: 1953525168

Partitions aligned to 16065 sector boundaries, offset 63

Partition table:
0: NetBSD (sysid 169)
bootmenu: NetBSD
start 2048, size 1953523120 (953869 MB, Cyls 0/32/33-121601/80/63), Active
1: <UNUSED>
2: <UNUSED>
3: <UNUSED>
Bootselector disabled.
First active partition: 0
Drive serial number: 0 (0x00000000)

The previous fdisk command also provided key information about the size of the hard drive . From the disklabel geometry, we get total sectors: 1953525168, bytes/sector: 512 which produces 1000204886016 bytes , approximately 931 GB

# fdisk -u wd0
Disk: /dev/rwd0
NetBSD disklabel disk geometry:
cylinders: 1938021, heads: 16, sectors/track: 63 (1008 sectors/cylinder)
total sectors: 1953525168, bytes/sector: 512

BIOS disk geometry:
cylinders: 1023, heads: 255, sectors/track: 63 (16065 sectors/cylinder)
total sectors: 1953525168

Partitions aligned to 16065 sector boundaries, offset 63

Do you want to change our idea of what BIOS thinks? [n]
fdisk command running interactively on disk wd0

Initialize the partition table

If the disk we are using has a previous partition table and/or disklabel, it is a good idea to initialize it, so we don’t get spurious data.

We run again fdisk on the target drive in interactive mode ( fdisk -u wd0 ). This time, we select the partition to initialize and enter the sysid 0 that will set it to UNUSED

Which partition do you want to change?: [none] 0
The data for partition 0 is:
NetBSD (sysid 169)
bootmenu: NetBSD
start 2048, size 1953523120 (953869 MB, Cyls 0/32/33-121601/80/63), Active
sysid: [0..255 default: 169] 0

Partition table:
0: <UNUSED>
1: <UNUSED>
2: <UNUSED>
3: <UNUSED>
Bootselector disabled.
No active partition.
Drive serial number: 0 (0x00000000)
Which partition do you want to change?: [none]

Installed bootfile doesn't support required options.
Update the bootcode from /usr/mdec/mbr? [n] y

Removing the current disklabel information

We have removed the partition 0, but the disklabel is still present. We will remove it with the following command:

# disklabel -D wd0

Partition layout

We will be using a single MBR slice t hat will hold the root filesystem and a large CGD partition to host the encrypted filesystems and swap.

MBR (use fdisk command)
0 .- 950 GB (sysid 169 – NetBSD). Bootable (“active”)
1-4: <UNUSED>

Disklabel partitions (use disklabel command)
a: 50 GB (4.2BSD to mount the unencrypted root filesystem)
b: swap (we’ll use swap in the CGD volume)
c: entire disk
e: 900 GB (CGD volume to hold encrypted /home, /usr, /var and swap partitions)

Disklabels have also conventions. The letter “a” holds the root filesystem. “b” is traditionally for swap, “c” is the entire disk (is managed by the kernel and can not be modified) and “e” is the MBR

Creating the MBR NetBSD partition with fdisk

We run fdisk command interactively to set the type (sysid) of the partition to NetBSD, assign a size of 950 GB and set it to bootable (‘active’).

# fdisk -u wd0
# fdisk -u wd0
Disk: /dev/rwd0
NetBSD disklabel disk geometry:
cylinders: 1938021, heads: 16, sectors/track: 63 (1008 sectors/cylinder)
total sectors: 1953525168, bytes/sector: 512

BIOS disk geometry:
cylinders: 1023, heads: 255, sectors/track: 63 (16065 sectors/cylinder)
total sectors: 1953525168

Partitions aligned to 2048 sector boundaries, offset 2048

Do you want to change our idea of what BIOS thinks? [n]

Partition table:
0: <UNUSED>
1: <UNUSED>
2: <UNUSED>
3: <UNUSED>
Bootselector disabled.
No active partition.
Drive serial number: 0 (0x00000000)
Which partition do you want to change?: [none] 0
The data for partition 0 is:
<UNUSED>
sysid: [0..255 default: 169]
start: [0..121601cyl default: 2048, 0cyl, 1MB]
size: [0..121601cyl default: 1953523120, 121601cyl, 953869MB]
bootmenu: [] (space to clear)NetBSD

Partition table:
0: NetBSD (sysid 169)
bootmenu: NetBSD
start 2048, size 1953523120 (953869 MB, Cyls 0-121601/80/63)
1: <UNUSED>
2: <UNUSED>
3: <UNUSED>
Bootselector disabled.
No active partition.
Drive serial number: 0 (0x00000000)
Which partition do you want to change?: [none]

Installed bootfile doesn't support required options.
Update the bootcode from /usr/mdec/mbr_bootsel? [n] y

We haven't written the MBR back to disk yet. This is your last chance.
Partition table:
0: NetBSD (sysid 169)
bootmenu: NetBSD
start 2048, size 1953523120 (953869 MB, Cyls 0-121601/80/63)
1: <UNUSED>
2: <UNUSED>
3: <UNUSED>
Bootselector enabled, timeout 10 seconds.
No active partition.
Drive serial number: 0 (0x00000000)
Should we write new partition table? [n] y
#

Set the ‘active’ (bootable) partition

We set the NetBSD partition (0) to be active (bootable)

# fdisk -a0 wd0
# fdisk -a0 wd0
Disk: /dev/rwd0
NetBSD disklabel disk geometry:
cylinders: 1938021, heads: 16, sectors/track: 63 (1008 sectors/cylinder)
total sectors: 1953525168, bytes/sector: 512

BIOS disk geometry:
cylinders: 1023, heads: 255, sectors/track: 63 (16065 sectors/cylinder)
total sectors: 1953525168

Partitions aligned to 16065 sector boundaries, offset 63

Partition 0:
NetBSD (sysid 169)
bootmenu: NetBSD
start 2048, size 1953523120 (953869 MB, Cyls 0/32/33-121601/80/63)
Do you want to change the active partition? [n] y
Choosing 4 will make no partition active.
active partition: [0..4 default: 0]
Are you happy with this choice? [n] y

We haven't written the MBR back to disk yet. This is your last chance.
Should we write new partition table? [n] y

Bootstrapping

To make the system bootable, we need to install the bootstraps with installboot .

# installboot -v /dev/wd0a /usr/mdec/bootxx_ffsv1 /boot


# installboot -v /dev/wd0a /usr/mdec/bootxx_ffsv1 /boot
File system: /dev/rwd0a
File system type: ffs (blocksize 16384, needswap 0)
Primary bootstrap: /usr/mdec/bootxx_ffsv1
Secondary bootstrap: /boot
Boot options: timeout 5, flags 0, speed 9600, ioaddr 0, console pc

Assigning the main disklabel partitions

We will create the disklabels in two steps. The first step will create the disklabel for the root partition (which will hold the root (‘/’) filesystem and the partition reserved to the CGD volume.

Once these two partitions are created, we will run again the disklabel command, this time using the cgd0 pseudo device.

We use disklabel interactively to create 2 partitions (a and e). Remember that c and d are reserved. ‘c’ partition represents the NetBSD partition on the MBR and ‘d’ the whole disk.

Partition ‘a’ (wd0a) will hold the root filesystem and partition ‘e’ (wd0e) the CGD volume.

# disklabel -iI wd0
# disklabel -iI wd0
Enter '?' for help
partition>a
Filesystem type [unused]: 4.2BSD
Start offset ('x' to start after partition 'x') [0c, 0s, 0M]: 2048s
Partition size ('$' for all remaining) [0c, 0s, 0M]: 51200M
a: 104857600 2048 4.2BSD 0 0 0 # (Cyl. 2*- 104027*)
partition>e
Filesystem type [4.2BSD]: cgd
Start offset ('x' to start after partition 'x') [2.0317461490631103515625c, 2048s, 1M]: a
Partition size ('$' for all remaining) [1938018.875c, 1953523120s, 953868.6875M]: $
e: 1848665520 104859648 cgd # (Cyl. 104027*- 1938020)
partition>W
Label disk [n]?y
Label written
partition>Q

A section of the output from the command disklabel wd0 shows the partitions on the NetBSD MBR slice. I have


size offset fstype [fsize bsize cpg/sgs]
a: 104857600 2048 4.2BSD 0 0 0 # (Cyl. 2*- 104027*)
c: 1953523120 2048 unused 0 0 # (Cyl. 2*- 1938020)
d: 1953525168 0 unused 0 0 # (Cyl. 0 - 1938020)
e: 1848665520 104859648 cgd # (Cyl. 104027*- 1938020)

Create the CGD (Cryptographic Disk Driver) volume

Now that we have the CGD partition created on wd0e , we need to initialize the CGD volume, that itself will hold the operating system filesystems encrypted (except root).

Generate the parameters file for the CGD volume, using the adiantum cipher and disklabe as the verification method.

# cgdconfig -g -V disklabel -o /etc/cgd/wd0e adiantum
pkcs5_pbkdf2: calibrating iterations................. done

The following /etc/cgd/wd0e is generated:

algorithm adiantum;
iv-method encblkno1;
keylength 256;
verify_method disklabel;
keygen pkcs5_pbkdf2/sha1 {
iterations 292728;
salt AAAAgPBuE6TEwv2TqPo4rowkEt8=;
};

As mentioned in the NetBSD guide related chapter , this file is critical , so make sure you back it up.

At this point, we are ready to create the actual CGD volume. The following command will ask us to enter the password that will later unlock the encrypted device.

# cgdconfig -V re-enter cgd0 /dev/wd0e
/dev/wd0e's passphrase:
re-enter device's passphrase:
#

The CGD partitions

The newly created CGD volume cgd0 behaves the same as another disk. We can now move on to creating the CGD partitions.

# disklabel -iI cgd0

We repeat the steps to create the partitions in CGD similarly as we did in the MBR slice. After we create and write the contents to disklabel, we end up with this list:

cgd0
6 partitions:
# size offset fstype [fsize bsize cpg/sgs]
a: 1638400000 0 4.2BSD 0 0 0 # (Cyl. 0 - 799999)
b: 20480000 1638400000 swap # (Cyl. 800000 - 809999)
d: 1848665520 0 unused 0 0 # (Cyl. 0 - 902668*)
e: 102400000 1658880000 4.2BSD 0 0 0 # (Cyl. 810000 - 859999)
f: 61440000 1761280000 4.2BSD 0 0 0 # (Cyl. 860000 - 889999)

Creating the filesystems

We now proceed to create the filesystems in their respective partitions. We will be using the FFS filesystem with the command newfs for each target partition.

For instance, the following command will create the root (“/”) filesystem in the first partition (“a”) of the unencrypted device wd0

# newfs /dev/wd0a

If everything went well, you should see something like this:

/dev/rwd0a: 51200.0MB (104857600 sectors) block size 16384, fragment size 2048
using 278 cylinder groups of 184.19MB, 11788 blks, 23296 inodes.
super-block backups (for fsck_ffs -b #) at:
32, 377248, 754464, 1131680, 1508896, 1886112, 2263328, 2640544, 3017760, 3394976, 3772192, 4149408,
.......................................................................................................
#

To create the filesystems residing in the cdg0 drive we would do the same, looking at the disklabel partition table above. For example, to create the large /home filesystem at partition “a” of the encrypted volume, execute the following command:

# newfs /dev/cgd0a

Partition “e” of cgd0 will hold the “/var” filesystem.

# newfs /dev/cgd0e

Repeat the commands for the remaining partitions.

Note : The swap partition (cgd0b) is a special type. Do not create a filesystem there.

Preparing target mount points

Now that the partitions and filesystems have been created in the target drives, we need to populate them with the base system, packages and devices.

Mount the target root filesystem under “/mnt/target”

# mkdir /mnt/target
# mount /dev/wd0a /mnt/target

Mount the remaining target filesystems in temporary directory

# mkdir /mnt/target/home
# mkdir /mnt/target/var
# mkdir /mnt/target/usr
# mount /dev/cgd0a /mnt/target/home
# mount /dev/cgd0e /mnt/target/var
# mount /dev/cgd0f /mnt/target/usr

Double check that your mount points and allocated space

# df -h
# df -h
Filesystem Size Used Avail %Cap Mounted on
/dev/dk1 2.2G 1.6G 584M 74% /
tmpfs 3.7G 12K 3.7G 1% /tmp
/dev/wd0a 49G 8.0K 47G 1% /mnt/target
/dev/cgd0a 775G 4.0K 736G 1% /mnt/target/home
/dev/cgd0e 48G 2.0K 46G 1% /mnt/target/var
/dev/cgd0f 29G 2.0K 27G 1% /mnt/target/usr

Installing the binary sets

So far everything has gone smoothly. Now is time to extract the software sets that you wish.

The NetBSD installation guide says that we need to include at least “ base “, “ etc ” and a kernel as a bare minimum. Once we boot the system, we can later install additional package sets.

Make sure you are in the newly created root filesystem:

# cd /mnt/target

Installing the kernel:

# tar -xzpvf /amd64/binary/sets/kern-GENERIC.tar.xz
x ./netbsd

Installing ‘base’ and ‘etc’ binary sets:

# tar -xzpf /amd64/binary/sets/base.tar.xz
# tar -xzpf /amd64/binary/sets/etc.tar.xz

Do the same for other packages you may want (“games”, “text”, “xserver”…)

Copying and adapting important files (cgd, fstab… )

Create the main CGD configuration file. This is important because CGD must be enabled before the filesystems are mounted .

# echo "cgd0    /dev/wd0e" > /mnt/target/etc/cgd/cgd.conf

Copy the current CGD parameter / cipher file:

# cp /etc/cgd/wd0e /mnt/target/etc/cgd/

Enable CGD at boot time:

# echo "cgd=YES" >> /mnt/target/etc/rc.conf

Create the target fstab file with the new filesystem entries

Making the devices in the target drive

As the MAKEDEV script says, “all” makes all known devices, including local devices

# cd /mnt/target/dev
# sh MAKEDEV all

Mounting and preparing kernel/proc/tmpfs

# mkdir kern proc
# mount_kernfs kernfs /mnt/target/kern
# mount_procfs procfs /mnt/target/proc/
# mount_tmpfs tmpfs /mnt/target/var/shm
# mount_ptyfs ptyfs /mnt/target/dev/pts

Chroot to the new drive

Getting closer… now we need to chroot to the new environment , so we can update the root password, update the fstab file.

# chroot /mnt/target su -

Create the fstab file with the folllowing entries

# vi /etc/fstab

# The root filesystem (unencrypted)
/dev/wd0a / ffs rw 1 1
# Swap, home, var and usr live in the encrypted CGD volume
/dev/cgd0b none swap sw 0 0
/dev/cgd0a /home ffs rw 1 2
/dev/cgd0e /var ffs rw 1 2
/dev/cgd0f /usr ffs rw 1 2
# kernel, proc, pty and tmp filesystems
kernfs /kern kernfs rw
ptyfs /dev/pts ptyfs rw
procfs /proc procfs rw
/dev/cd0a /cdrom cd9660 ro,noauto
tmpfs /var/shm tmpfs rw,-m1777,-sram%25

Double check that the devices / partitions match your installation!

Add the new root password

# passwd

Include additional / optional entries to /etc/rc.conf

You can include or customize additional services in your rc.conf. For example, the hostname, mail server or DHCP client. Some common entries are:

hostname=tolstoy.gnuhealth.org (change it to your hostname)
wscons=YES # The NetBSD console subsystem
dhcpcd=YES # Activate DHCP
postfix=NO #Disable mail server

Rebooting the system to the newly installed NetBSD

If everything went well, then you should be happily booting into your new hard drive. Remember that since we have

From this point, you can explore different services, configure the package system (pkgin), set up the graphical interface and install cool games. The Sysinst program allows you to do post-installation tasks. This is just starting!

fastfetch program running in NetBSD .
OS: NetBSD 11.0 amd64
Host: HP Pavilion dv6 Notebook PC (048E100000242B10000020000)
Kernel: NetBSD 11.0
Uptime: 1 hour, 2 mins
Packages: 7 (pkgsrc)
Shell: sh
Display (LVDS-1): 1366x768, 60 Hz [Built-in]
Terminal: /dev/pts/1
CPU: Intel(R) Core(TM) i7 Q 720 (8) @ 1.47 GHz
Memory: 164.63 MiB / 3.79 GiB (4%)
Swap: 0 B / 9.77 GiB (0%)
Disk (/): 171.06 MiB / 49.22 GiB (0%) - ffs
Disk (/home): 28.00 KiB / 775.12 GiB (0%) - ffs
Disk (/usr): 1.02 GiB / 28.84 GiB (4%) - ffs
Disk (/var): 62.75 MiB / 48.07 GiB (0%) - ffs
Local IP (re0): 192.168.1.153/24
Battery: 100% [AC Connected]
Locale: C
Post-install information of the NetBSD system

An operating system made by humans, for humans

Last but not least… I wrote about generative AI / LLM becoming the new pandemic and why we need to find ethical Free/Libre Software alternatives for our computing and for our society. NetBSD is one of the projects that took a stance against the use of genAI and that by itself deserves our support, respect and adoption.

Resources

Writing this post has been a lot of fun and a fantastic learning experience to dive into the NetBSD internals. The following resources have been very helpful and inspiring. They are mainly focused in UEFI, but a lot of information is also valid for MBR systems, and you will probably have a UEFI system anyways 🙂

Thank you all for sharing your time, knowledge and talent. ♥

PS: I am sure there are errors and better ways to implement any of the processes in this post. Please ping me and we update it! You can find me in Mastodon ( https://todon.eu/@meanmicio ).

Happy hacking!

Iustin Pop: AI agents aha moment

PlanetDebian
k1024.org
2026-09-06 09:29:50
Looking at the reactions to the Debian AI vote, I think some people still think the clock can be turned back, as if that ever worked in history. Rather than cry about spilled milk, I prefer to find a path forward in the new world. There are many ways to use LLMs, some of them are straightforward, ot...
Original Article

Looking at the reactions to the Debian AI vote, I think some people still think the clock can be turned back, as if that ever worked in history. Rather than cry about spilled milk, I prefer to find a path forward in the new world. There are many ways to use LLMs, some of them are straightforward, others not so much.

One of the “not so clear” areas for me is the focus on agentic workloads. For complex tasks, sure, you want something that can work in the background, but in general, why does every single tool go the agentic way? I much prefer the “chat/ask” approach, or even the “code” one, but if I’m at the keyboard, why would I send a task to an agent, and see it work, instead of directly implementing it?

And then, this past Friday, I finally understood one part of that. I was in the airport, sitting at the gate and waiting to board a flight, and because I arrived much earlier at the airport (fearing crowds due to Labour Day weekend), I got one hour of work before boarding started. As the time for boarding approached, I did one more commit after making sure tests pass, pushed, closed laptop, and went to walk a bit before getting on the plane.

As I was getting up, I get a phone notification from GitHub that the CI run failed . I was quite surprised, as the local tests passed, so I open the notification, and realize that tests via make test vs CI (which additionally uses --pedantic ) had slightly different settings, and of course I missed a build warning (which in CI is an error).

I thought I’d fix that on the plane, but then I saw a “Copilot agent” button in the mobile app. I was curious what it did, I click it, and I see Copilot starting a draft pull request , and saying:

Thanks for asking me to work on this. I will get started on it and keep this PR’s description up to date as I form a plan and make progress.

Fix the failing GitHub Actions job. Analyze the Actions logs, identify the root cause of the failure, and implement a fix.

Then it goes, finds the failure, writes the fix, and tries to run the tests. Well, it can’t do it (it runs in a restricted container, so no network, so stack install couldn’t actually work). The agent sees that, acknowledges it has no way to validate the fix, but the error message was clear enough that it was confident the fix is mostly correct, so it sends the pull request.

I allow full CI to run on the pull request, and go buy a bottle of water. After that, I check and see that the CI failed again, as not one but two test files were broken, and I didn’t have --keep-going , so the build stopped at the first failure. I write a comment in the pull request, no reaction, I realize I need to tag Copilot explicitly, I do that, and it starts another investigation.

I’m waiting now in the boarding queue, with phone in hand, while Copilot is fixing my bug. While I scan my boarding pass and walk towards the plane, the pull request is updated, I trigger another CI, it passes, and I merge it.

And then, it hit me. Agents allow me to make progress while being “not at keyboard”, whether that’s physically “not at keyboard”, or while working on something else. Fixing a simple test failure is not something that needs human attention per se, whereas improving the test layout might be.

In that airport, using otherwise-unusable downtime, and without explicitly intending to, I made progress in understanding a different way to use AI. Now I have three ways to work with LLMs: ask (tutor mode), code (implement my request), and agent (fix simple or complex problems, autonomously). I still don’t know about “plan” mode and really complex tasks, like asking it to implement features from scratch. That will probably be the next area to tackle.

And today (Sunday), while waiting for a running race to start, I opened GitHub, and asked Copilot to increase test coverage for a simple module. It did, and yes it still can’t run tests (I learned in the meantime that you can configure the environment in which the agent runs, nice), but after two back-and-forth messages, I have a pull request ready to review. All in the 20 minutes before a race, where I could either browse social media or actually do some meaningful work.

Checking now my GitHub billing, it looks like all of this Copilot use only cost $1.92. Yes, that is under two dollars! And while it did use compute resources, the person across the aisle who watched TikTok or Instagram for half an hour while waiting for takeoff also consumed a lot of compute, and so do the gazillion cat videos uploaded to YouTube every day.

To me, this is another tool in the toolbox, that might one day replace me (as it did to the 19th-century textile workers), or make me five times more productive — we’ll see where we end up. In the meantime, I can move faster, and make better use of my limited free time.

Enjoy the ride!

Dirk Eddelbuettel: RcppFarmHash 0.0.4 on CRAN: Maintenance

PlanetDebian
dirk.eddelbuettel.com
2026-09-06 09:29:00
Another minor maintenance release of the RcppFarmHash package is now on CRAN as version 0.0.4. RcppFarmHash wraps the Google FarmHash family of hash functions (written by Geoff Pike and contributors) that are used for example by Google BigQuery for the FARM_FINGERPRINT digest. This releases updates ...
Original Article

RcppFarmHash 0.0.4 on CRAN: Maintenance

Another minor maintenance release of the RcppFarmHash package is now on CRAN as version 0.0.4.

RcppFarmHash wraps the Google FarmHash family of hash functions (written by Geoff Pike and contributors) that are used for example by Google BigQuery for the FARM_FINGERPRINT digest.

This releases updates several of package internal files for continuous intergration and package data.

The brief NEWS entry follows:

Changes in version 0.0.4 (2026-09-06)

  • Minor updates to continuous integration, README.md and DESCRIPTION

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

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

/code/rcpp | permanent link

Icy Moons Are Ocean Worlds

Hacker News
mceglowski.substack.com
2026-09-06 09:07:52
Comments...
Original Article

If you’ve ever spent time with an 11-year-old during their dinosaur phase, you know the feeling of having everything you thought you knew upturned by a pitiless pedant. Science moves on and leaves whatever we learned in school hopelessly out of date. The dinosaurs I grew up with were slow thinking and cold-blooded monsters. They came in muted shades of ugly brown and green, like Subarus. I was taught that the brontosaurus was so chunky it had to spend its life submerged in marshes to help buoy its weight, while the T-rex staggered around on its back feet like Godzilla, waving its little arms.

But starting in the 1990’s, dinosaurs started to get cooler—suddenly they were running fast, covered in feathers, hunting in packs. The T-Rex got upgraded to a high-speed, warm-blooded killing machine. The dinosaur entertainment complex rolled out a whole new set of small, intelligent hunter-killers. And at some point the brontosaurus got cancelled and doesn’t even exist anymore, subdivided into three new dinosaurs I had never heard of.

Something similar has happened to the frozen worlds of the outer solar system. In a series of glow-ups, they’ve gone from being a sort of Space Antarctica of interest only to the most spectrumy of ice nerds, to a series of water worlds that are the most likely environment in our solar system to harbor life. Even poor Pluto, demoted from planethood back in 2006, has been revamped to a candidate ocean world, with the implication that the thousands of Pluto-like objects still undiscovered in the Kuiper belt may be harboring secret seas of their own. Today you can hardly swing a telescope without pointing it at a celestial body hiding a warm underground ocean.

This remarkable transformation in our understanding is the result of just three missions: Voyager , Galileo , and Cassini —along with some computer modeling and hard staring by the Hubble and Webb space telescopes.

As of this writing, six icy worlds (Europa, Enceladus, Titan, Mimas, Callisto and Ganymede) are confirmed to have vast underground oceans of liquid water, and a bunch more (Dione, Pluto, Miranda, Ariel, Triton, Oberon) are on the waitlist.

Let’s meet the crew!

Of the six worlds so far shown to have underground oceans, three are moons of Jupiter, and three are moons of Saturn.

Europa is the OG ocean world and a bit of a celebrity for that reason. When Voyager flew through the Jupiter system in 1979, scientists were amazed to discover active volcanoes on Io, confirming a prediction published just a week earlier (!) that tidal heating could substantially warm the inner moons of Jupiter. Since Europa was the next moon out from Io, it stood to reason that tidal heating might be at work there too, an impression reinforced by the craterless smoothness of its surface. But whether Europa was an actual ocean world, or just covered in warm convecting ice, was not definitively settled until 2000, when magnetic evidence for an ocean became overwhelming.

The radiation environment around Europa is punishing; an astronaut standing on the surface would get a fatal dose in about a day. 1 But the same radiation means the ice crust is enriched in molecular oxygen and peroxides (created when water molecules are split by radiation) that may cycle down into the planetary ocean, creating a rich potential environment for life.

Hubble has seen plumes coming out of Europa, but the observations remain tentative. Hopefully Europa Clipper will settle the matter when it arrives at the moon in 2031.

Ganymede, the next furthest moon out after Europa, is kind of a reverse Pluto. By all rights it should be a planet—it’s bigger than Mercury and has its own magnetic field—but Fate has placed it in orbit around Jupiter, and that is where it is going to stay. Ganymede is not a tectonically active world like Europa, and its ocean is locked away under a hundred miles or more of ice. But despite the gruff exterior, the moon shows surface signs of an active past, and contains enough rock (with its cargo of radioactive elements) to stay toasty on the inside, maintaining not only a liquid core, but the largest known ocean in the Solar System.

Ganymede’s intrinsic magnetic field made it hard to apply the same techniques that proved the existence of oceans on Europa and Callisto. But careful observations of aurorae on Ganymede by the Hubble telescope in 2011 showed them to be shifting in a way that only the presence of a global ocean could explain.

Callisto is the outermost of the four large Jovian moons. Where Europa has some of the newest crust in the solar system, Callisto has the oldest, a surface so cratered there is simply no way to crater it further. Callisto is also poorly differentiated, meaning that its interior is a jumble of ice, rock, and small amounts of metal that have not settled into distinct layers. Voyager showed Callisto to be Jupiter’s punching bag, sitting out there in a cold orbit, absorbing impacts. But to everyone’s surprise, magnetic measurements by the Galileo orbiter showed evidence of a deep, liquid ocean, making Callisto the first candidate ocean world in 1998. Absent any sign of surface activity, the inaccessible ocean 150 kilometers under Callisto’s crust is one of the most isolated habitats in the Solar System, cut off from the outside for over four billion years. Whatever may be down there is not coming up without a fight.

And now for the three moons of Saturn:

Enceladus is far smaller than the Jovian ocean worlds, roughly the size of Ohio. But it is a much more exciting place than Ohio, and far more livable.

Enceladus rivals Europa as the most promising candidate for life in the solar system. The ‘tiger stripe’ features on its southern hemisphere send giant plumes of seawater into space, and in 2008 the Cassini probe was able to fly through one and taste the Enceladan ocean directly. Chemical analysis of the plumes and surface has found salty water, all six of the elements necessary for terrestrial life, phosphates, unidentified organics, hydrocarbons, and a kiss of cyanide. Cassini also detected silica dust and molecular hydrogen (potential microbe kibble) originating in undersea rock, the first direct detection of a water/rock interface. At this point the only way for Enceladus to be more habitable would be if we found dense, walkable neighborhoods and an IKEA.

Enceladus was shown to have at least a regional ocean in 2014, and observations upgraded this to global status the next year. It remains the only alien sea we have been able to sample directly.

Somewhat incredibly, there’s no mission in the pipeline to visit Enceladus, even though doing so would cost less than the $5B NASA will spend flying Artemis III to low Earth orbit in 2027.

Titan is the most enigmatic world in the solar system. Barely losing the ‘biggest moon’ contest to Ganymede, it has a nitrogen atmosphere dense enough that an astronaut wearing a wingsuit could fly around just by flapping. It’s worth stressing that, unlike the other moons in this list, Titan is astronaut friendly—the thick atmosphere is a better shield against radiation than what we have on Earth, the low gravity (0.14g) makes getting around a breeze. All you really have to do is remember to bring oxygen and a sweater.

Like those lottery scratch-off tickets that give you a second chance at winning, Titan offers two distinct chances at finding life. On the surface there is a very famliar landscape of rivers, streams, lakes and rainfall, except that the lakes and raindrops are made of liquid hydrocarbons like ethane, with water ice playing the role of rocks. If life exists in this complex surface environment, it resembles nothing we know or can easily imagine, which is part of what makes the prospect of finding it so exciting.

Underneath this remarkable landscape, Titan is an ocean world, with a salty subsurface sea that could be home to more recognizable forms of biochemistry, especially if it is able to interact with the organic-rich surface. The sea was first detected through orbital analysis in 2012, corroborating observations made in 2005, when the Huygens lander observed a radio wave resonance suggestive of an underground salty ocean. As of 2025, there is controversy over whether Titan has a genuinely world-spanning ocean, or whether it is more of a slush of ice and meltwater, but from the point of view of astrobiology, both options are exciting and livable.

What exactly is going on on Titan should become clearer when the Dragonfly probe lands on the moon sometime in 2034.

Mimas is the newest and least expected addition to the ocean world roster. A tiny moon notorious for looking just like the Death Star , it appears far too frozen and rough to sport a liquid ocean, which would be expected to soften its features, especially the giant marquee crater. For a while scientists were positing an alternative explanation for its orbital behavior (an oblong silicate core), but around 2024 they gave up and made peace with the ocean.

The conjecture is that the ocean on Mimas formed recently, a result of orbital changes within the last few million years, and the crust hasn’t had time to get the memo about the new interior. The existence of a surprise ocean raises the likelihood of finding more ‘stealth’ ocean worlds among the minor outer moons.

To put the known ocean worlds in perspective, here’s a schematic of what’s going on in the top 1000 kilometers or so of each of them:

As you can see, the ocean worlds divide into two groups.

Ganymede, Callisto, and Titan all have oceans deep enough for high-pressure ice to form along their bottom. This ice has no real counterpart on Earth—pockets of it may exist in the mantle, but our oceans would have to be some 50 km deeper before the pressure got high enough to form it. This ice layer has the effect of separating the liquid ocean from the rock underneath, which is bad—you want the two to mingle so all kinds of useful salts and minerals can leach into the water.

In addition to this high-pressure ice layer, Ganymede and Callisto also have an extremely thick upper crust that seems to be geologically stagnant, making them less appealing exploration targets.

Europa, Enceladus, and Mimas have a thinner ice shell on top and liquid water in direct contact with a rocky mantle (or core). From a habitability perspective, this is exciting, since rock/water reactions on Earth drive a lot of the chemistry necessary for life. And of course, the thinner shell creates the possibility that we could one day sample these oceans without having to figure out how to make a robot drill through a hundred miles of primordial ice.

But I should stress how provisional these models are. Everything in the diagram above is a guesstimate based on orbital perturbations and fairly primitive computer models. In particular, the models are very sensitive to the chemical composition of ocean water, something hard to observe with remote sensing, and critical to the inner structure of each moon.

For example, if you plug some realistic salt assumptions into the model for Ganymede, you get a ‘club sandwich’ version of the world, with multiple layers of high-pressure ice separated by thin oceans of brine, and a final liquid layer sitting on top of bedrock. Is this configuration realistic? Stable? Habitable? A lot of these questions have to wait until we can land a seismograph or two.

Candidate ocean worlds. Green are confirmed, purple likely, orange possible. Diagram adapted from Henin (see below).

In addition to the six known ocean worlds, there is a whole bestiary of candidates just waiting for us to come take a closer look.

Saturn’s moon Dione likely had an ocean in the past, but whether that ocean still exists or has frozen solid is not known.

Most of what we know about the moons of Uranus comes from a single flyby by Voyager 2 in 1986. That encounter showed the moon Ariel to have a very active surface, second in the solar system only to Enceladus. Ariel has spent time in the right kinds of orbit to experience tidal heating (the mechanism that melts Io), and during its flyby, Voyager 2 even detected material consistent with an Enceladus-like plume. But no spacecraft has visited Uranus since, and the data is just too sparse.

All the more so for Titania and Oberon . Models show that both worlds could have a subsurface ocean in contact with rock (good!), but at extremely cold temperatures that would require a lot of ammonia and other antifreeze compounds (bad!).

Neptune’s moon Triton is a close relative of Pluto that somehow got captured into a weird orbit around the ice giant. Voyager 2 observed active plumes on Triton, although these were likely shallow phenomena caused by sunlight shining on dark material through nitrogen ice. More exciting were the observations of a very young crust, showing that Triton still packs enough heat to resurface itself on the regular.

The same holds true for Triton’s cousin Pluto , which the New Horizons probe showed to be a geologically active world that almost certainly has a deep ocean 2 . Modeling shows that a properly insulated ice layer on Kuiper Belt objects the size of Triton or Pluto could sustain liquid oceans for billions of years, with only radioactive rock to warm them.

This opens the door to some dizzying prospects. Rogue planets are believed to outnumber the stars in our galaxy by perhaps 10:1, and many of them could be traveling with ocean moon companions that could remain habitable for billions of years. Any life out there would exist in unimaginable darkness, finally breaking through a thick prison of ice only to discover itself orbiting a black planet under a sunless sky.

Closer to home, the Kuiper Belt is likely full of slowly freezing ocean worlds in the mold of Pluto, which could number in the hundreds.

With such a deep bench to choose from, which moons are worth exploring first? There are several criteria everyone agrees on.

  1. Water in contact with rock . A lot of interesting things happen at rock/water interfaces, and several elements believed to be essential for life (phosphorus, sulfur, metal ions) need to leach out of silicate rock to be chemically available in ocean water. Rock/water interactions also create molecular hydrogen, which computer models have shown could be an abundant enough food source to sustain an Earth-sized ocean ecosystem for billions of years.

    That said, the high-pressure ice lining the ocean bottom on Ganymede, Titan, and Callisto might not be a showstopper—it’s possible that it convects, or that material erupts through it, preserving the connection between deep rock and ocean. Clearly at least some of it is getting through, since the oceans are salty. But given a choice, we want to prioritize worlds where we know liquid water flows through rock.

  2. Antiquity . Since we have no idea how long it takes life to arise, it’s prudent to explore oceans that have been around for a few billion years. This is easier said than done. Moon orbits in the outer solar system are chaotic, and oceans may experience multiple freeze/thaw cycles over the aeons. This is the one place where Europa outshines Enceladus, since the former almost certainly has a primordial ocean, while the Enceladan ocean is of indeterminate age, and might be only a few hundred million years old.

  3. Remodeling . The ideal is the smooth, almost craterless flatness of Europa or Triton, which suggests active turnover between the surface and interior. Jupiter has contenders at both extremes, with the crust of Europa being about 50 million years old (basically brand new), while Callisto is the most heavily cratered body in the solar system, a poor moon that has done nothing tectonically for its entire existence except serve as a punching bag for meteors.

    The appeal of a young crust is that it implies dynamic movement and recirculation from below, either through convection (warm ice flows readily) or by cryovolcanoes erupting and coating the surface. Both mechanisms bring material from deep underground to the surface (great for the search for life!) and by symmetry carry material from the surface down to the ocean, which plays an important role on radiation-fried moons like Europa, where the top layer of ice gets enriched with enough oxygen to fuel an entire ocean.

  4. Radioactive rocks . Radioactive elements like potassium and thorium are Nature’s electric blanket, helping keep even the iciest worlds toasty at the core. Even on a tidally heated world like Europa, much of the nternal heat still comes from radionuclides, and they are the main sustainers of heat on remoter worlds like Pluto. The gold standard would be to find a moon that formed early enough to capture some aluminum-26, a short-lived isotope present during the early years of the solar system that would have really brought the heat.

  5. Plumes . Plumes can save you big money on a lander by propelling the contents of an ocean directly into space, where it can be sampled by passing spacecraft. If we’re really lucky, a big vent may spew remnants of some space fish directly onto the surface ice, saving us a long and contentious search for life. Plumes and vents might also sustain some kind of weird ecosystem along their edges, the way subsea hydrothermal vents do on Earth. It’s not likely that an alien squid is going to hit the windshield of our next Enceladus orbiter, but it’s not out of the question, either, and that’s why everyone loves a plume.

Together these factors explain why Europa and Enceladus are such attractive targets for astrobiology.

But in starting to look for life on these moons, we have to be careful to not let our thinking get too uptight.

QBittorrent breaks out of sandbox to commit crimes

Hacker News
beige.party
2026-09-06 09:02:41
Comments...

I'm teaching an introductory 12 week course on Quantum Oracle Engineering

Hacker News
shukla.io
2026-09-06 09:00:34
Comments...
Original Article

Most quantum speedup claims depend on an oracle that exists only on paper. This course teaches the craft of building practical quantum circuits from scratch.

Choose the problem, build the oracle

1. A different computer

  • CPU, GPU, QPU : three devices, three workloads
  • the QPU’s job: fewer samples for an average
  • order one over epsilon queries in place of order one over epsilon squared samples
  • three questions : task randomness, precision, oracle cost
  • Grover on a database loses to data loading
  • break-even: t oracle less than t roll over C times P times g

2. The Monte Carlo speedup

Two dice tumbling across a green felt table
  • a query count is not a runtime
  • the payoff qubit’s angle encodes the win probability
  • amplitude estimation reads that angle to precision ε
  • best of k arms: omega of k over epsilon squared samples vs order root k over epsilon, up to logs queries
  • Go fails question 1, the bandit fails question 3
  • Sway : gaps of 10⁻⁴ on a 32×32 board
  • the same oracle shape fits an epidemic model

3. Ship it

An open crate of machine parts sitting on an unread blueprint
  • the contract: board, two moves, randomness tape, payoff qubit
  • one round: Black places, White places, every stone rolls
  • the register layout in Qiskit
  • a uniform move choice over the legal cells
  • the d20 as a 5-bit comparison against a neighbor count
  • 3×3, two rounds: 169 qubits

4. Reversible by design

A cassette tape with its ribbon spooled out in a loop behind it
  • amplitude estimation runs the rollout forward and backward
  • decide from the old board, write to a shadow board , keep the old one
  • in-place updates read a neighbor that already flipped
  • erase move-selection scratch before the board changes
  • one payoff qubit , everything else inverted
  • the qubit and gate count as the board grows

Make it correct

5. Garbage collection

A trash can overflowing with bags that were never hauled away
  • reversible circuits have no delete
  • entangled scratch breaks interference
  • Bennett : compute, copy out, uncompute
  • the inverse must see the same inputs as the forward pass
  • peak scratch sets the qubit count
  • clean scratch is necessary, not sufficient

6. Measure to erase

A cracked jar of water patched with bandages, still leaking
  • the textbook says never measure mid-circuit
  • compilers measure scratch to reclaim qubits
  • Gidney’s AND† : an X-basis measurement instead of a Toffoli
  • a random sign, fixed by one phase gate
  • half the T gates of an adder
  • safe when scratch holds a basis function of the data

7. Calling conventions

Interlocking gears meshing edge to edge, every tooth having to fit its neighbor
  • three scratch classes: clean, borrowed, conditionally clean
  • Qiskit passes the reuse condition as unchecked convention
  • a block can destroy its own condition
  • two correct blocks, one unguaranteed boundary
  • restoration types : Hoare contracts over subspaces
  • a 12-bit oracle: 20 qubits to 13

8. Proof-carrying circuits

A sealed jar of identical blue pills with a single red one inside
  • truth tables cannot see a phase
  • full-basis checking costs 2ⁿ
  • certificates replayed by a Lean kernel
  • gate-by-gate checking needs closure under the gate set
  • past Toffoli , assertions grow exponentially
  • one theorem per family , checked in milliseconds

Count it, test it, judge it

9. Where the quantum lives

A hand pulling a book from a shelf, red light spilling from the gap
  • a process that runs step by step
  • between any two steps, classical bits would do
  • no one classical carrier works for all steps at once (Bisio)
  • the SHIFTS channel : one qubit in, two out, built to show it
  • the quantum lives in the memory between steps

10. All or nothing

One oversized box balanced on a stack of small ones: each piece is cheap, the whole is not
  • running n copies does not amortize
  • quantum memory: zero or linear in n, nothing between
  • log n and root n scalings ruled out
  • the same law for preparing states
  • SHIFTS: at least 0.03 qubits per copy
  • a theorem , with constants

11. Test, don’t trust

A basket of red apples with one rotten apple hidden in the middle
  • a test with single-qubit measurements only
  • a correct device passes every time
  • q qubits of memory pass with probability at most two to the q times one minus gamma to the n
  • too little memory fails exponentially fast
  • the device stays a black box

12. Audit the next claim

Pages flying off a clipboard faster than anyone can check them
  • the AI era’s assumption: compute closes every gap
  • the one over g squared wall : tiny gaps, irreducible randomness
  • weak baselines , query counts sold as runtimes
  • ignored parallelism , solver randomness as task randomness
  • oracle cost hidden behind “assume oracle access”
  • the three questions on a headline claim , live

Uncanny and unappetizing: appetites spoil as AI images take over food menus

Guardian
www.theguardian.com
2026-09-06 09:00:06
Consumers are increasingly encountering AI-generated images on food menus such as leathery meat and bread resembling reptile skin During a recent lunch break, Jill Sennett saw something so unappetizing she had to share it with her 26,000 X followers: Artificial intelligence-generated menu images fro...
Original Article

D uring a recent lunch break, Jill Sennett saw something so unappetizing she had to share it with her 26,000 X followers : Artificial intelligence-generated menu images from a Jamaican barbecue pop-up restaurant in which the meats looked like leather belts crawling with tiny beetles.

The images were reshared by more than 500 people who similarly expressed their disgust. Eating is “such an essential human experience”, said Sennett, a 37-year-old nurse in Denver. “I think it’s a bad cultural thing that restaurants are converting to these horrific, uncanny food images that are unappetizing.”

Still, Sennett ordered chicken and macaroni and cheese. It was one of her only lunch options, and she had eaten there before.

Increasingly, consumers are encountering AI-generated images of food on restaurant menus or marketing material. Diners like Sennett say the images are unappealing and misleading, while restaurants say they’re trying to cut costs at a time when food and labor prices are high , so profit margins are slim.

Artificial intelligence-generated menu images from a Jamaican BBQ pop-up restaurant.
A menu from a Jamaican barbecue pop-up restaurant. Photograph: Jill Sennett

A 2026 report from the National Restaurant Association found 26% of restaurant operators use AI to help with marketing, inventory management, employee scheduling, optimizing menus and taking orders.

But consumer backlash to the technology can be swift and fierce, especially in the Bay Area, a hotbed for AI development. When San Francisco cafe Grind & Unwind put up a sign depicting menu options that locals quickly identified as AI-generated, vandals graffitied the storefront with a word cafe owner Lyndsey Lozano deciphered as “seriously”. In a Reddit post titled “Yum, slop” , commenters compared the images of bread to textures like reptile skin and a loofah.

Lozano told SFGate in July that the response was “not what we were expecting”, and that the signage was only meant to be temporary. Lozano and her husband took down the AI signage, and spent an estimated $700 to paint over the graffiti, according to SFGate. Lozano did not respond to the Guardian’s request for comment.

Sennett takes some pleasure in AI fumbling food pictures so badly. “It can do uncanny videos of celebrities, but it can’t depict a hamburger,” Sennett said. “I hope it stays that way, honestly, and we can shame [restaurants] into stopping.”

The manager of Jamaican Jerk and Barbecue Restaurant , the Denver restaurant that hosted a pop-up at Sennett’s hospital, confirmed that he had used ChatGPT for the menu images instead of paying a graphic designer as he does for his bricks-and-mortar location.

“We decided we would design something that was eye-catching,” he said, declining to give his name for publication. “Restaurant people are trying to be cost-effective.”

As norms for AI in advertising are still being established, some restaurant industry professionals are thinking critically about when AI might be advantageous or detrimental to their businesses. Hunter Lewis, editor in chief of Food and Wine magazine, said he’s seeing restaurants grow as they begin to automate parts of their back-end operations. But use of the technology should be invisible to diners, as AI images can be “a real turnoff”, Lewis noted

“The American dining public is smart,” he said. “They know what they want, and they know what is real.”

Some restaurants are going viral for proclaiming they will never use AI and instead posting hand-drawn images of their burgers on social media. When Wyoming restaurant Chugwater Soda Fountain shared its pledge, written on a piece of cardboard, the Instagram post got over 200,000 likes. Some commenters celebrated the homespun authenticity, while others pointed out the irony of posting the message on an AI-powered platform.

Often the thing AI gets wrong is the food’s texture, said Jamie Soja, a professional photographer in the Bay Area who’s shot images for restaurant marketing and for food-delivery services.

“The color and texture and the ingredients look kind of off in the way that they’re arranged,” Soja said, adding that the lighting can also look unnatural.

AI enhanced images seen on DoorDash
AI-enhanced images seen on DoorDash. Photograph: Obtained by the Guardian

Tech companies, including food-delivery app DoorDash, are infusing AI into their platforms, intensifying the debate of AI-made menus. DoorDash offers AI photo tools “to improve the presentation of an existing image of a dish,” by adjusting the lighting, color or background, the company said in an emailed statement. The company’s policies prohibit eateries from creating or altering misleading images of menu items. DoorDash says it reviews menu images to ensure compliance.

A cursory search showed some images marked with DoorDash’s “AI-enhanced” label, which is automatically applied when a restaurant uses its AI editing tool. Still, it can be hard to determine whether images are AI-generated or simply overedited, even for a professional photographer like Soja.

Researchers are starting to study how seeing AI images of food affects consumers’ opinions and appetites. In a 2024 study Charles Spence, a University of Oxford professor of experimental psychology, found consumers generally preferred AI-generated food images when they didn’t know they were made by a machine. After that disclosure, however, diners found the images less appealing.

He found bots alter food in predictable ways: AI often adds fat, like butter on mashed potatoes, when prompted to make food images look more appealing. Spence worries that such AI images might subconsciously nudge diners and chefs toward larger portions with higher fat content.

AI images can leave a bad taste in consumers’ mouths, particularly those who work with the technology day in and day out. When Brandon Hill, CEO of a design and marketing agency in San Francisco, walked by Bella Cafe downtown recently, he was so put off by its AI images that he snapped a photo and shared it with his over 86,000 X followers with a Japanese caption. “Even in San Francisco … places are starting to use generative AI for menu images. But they don’t look all that appetizing, you know,” reads X’s English translation of the post.

AI images also make him “skeptical of what the actual meals will look like,” he said.

Despite Hill’s post, Nila Norero Salvatore, the owner of Bella Cafe, said she personally hasn’t received any complaints about the ChatGPT-designed signs on the street or those advertising the breakfast menu inside her restaurant, both of which include images of the food. The 65-year-old said she and her husband are “old-fashioned,” adding that experimenting with AI is “fun” and “new for us.” She also uses ChatGPT to help write reference letters for workers and craft the cafe’s employee handbook.

In addition to saving money on graphic design, Norero Salvatore likes how the AI images look.

“I think it’s a great way of advertising our products, even though sometimes [people] say AI isn’t effective or it’s not natural,” she said.

Schemy Lisp En DOS

Hacker News
sled.neocities.org
2026-09-06 08:58:54
Comments...
Original Article

Schemy LISP en DOS

About

SLED ( S chemy L isp e n D OS) is a purely symbolic LISP (LISt Processor) with functionality (largely) inspired by Scheme . Originally derived from the fantastic Kilo LISP , but reduced by some features (such as macros), and enhanced with others, SLED can be classified as an Ur-Lisp and runs on a DOS (Disk Operating System) such as FreeDOS or MS-DOS, as well as on DOS-emulators like DOSBox , DOSBox-X , or DOSBox-Staging . For an overview of provided symbols, special forms, builtin functions, and standard library see the index . Get SLED:

Overall, SLED is a LISP for DOS.

Data

There are two fundamental data types: Pairs and Atoms (not-pairs). Atoms come in three variants: Symbols , Closures (functions), and some Special Symbols .

Symbols

Symbols are unique names and consist of any combination of maximum 16 of the following characters:


a b c d e f g h i j k l m n o p q r s t u v w x y z 1 2 3 4 5 6 7 8 9 0 - . ? _

where . cannot be the leading character.

Additionally, any printable ASCII character can be part of a symbol when prefixed with the escape character \ (backslash), with the exception of ( , ) , ' , and $ .


This\ is\ a\ sym\!

Furthermore, uppercase letters are accepted but converted to lowercase unless the character is escaped.

Special Symbols

There are some predefined special symbols managed by SLED, for example nil which means "empty list". See the index for details.

Quote

A quote means "do not evaluate". Via the quote special form a symbol is registered:


(quote sym)

For convenience the ' short form syntax may be used:


'sym

Essentially quoting declares something as data instead of code.

Pairs

Pairs consist of a head and a tail , each holding either an atom or another pair . A pair can be created as data using the . (dot):


'(a . x)

or as result of the cons builtin function:


(cons 'a 'x)

Pair elements (head and tail) are immutable.

Lists

A list is a sequence of pairs where each tail points to a distinct other pair except one (the last) whose tail is the nil value, which is equivalent to () . Here are some lists:


nil
'()
'(a . nil)
'(a . (b . nil))

A list can be created as data also by:


'(a b)

or as result of the (standard library) list function:


(list 'a 'b)

Improper List

An improper list does not terminate by nil , for example:


(a . (b . (c . d)))

Association List

An association list is a list where each element is a pair (association):


((a . x) (b . y) (c . z))

The head part of such a pair element is called key and the tail is called value .

S-Expressions

A symbolic expression (S-expression) is a data structure defined as: An S-expression is either an atom or a pair of S-expressions. In Lisp, Scheme, and in particular in SLED, S-expressions are used for data as well as source code.

Numbers

The SLED system does not feature numeric types. Yet natural numbers (non-negative integers) can be emulated using lists:


'()             ; zero
'(nil)          ; one
'(nil nil)      ; two
'(nil nil nil)  ; three

These are tally numerals, so cardinality represents the magnitude, which is similar to von Neumann ordinals. The functions inc , dec , and zero? facilitate with counting tasks.

Code

In LISP, unquoted data is evaluated as code.

Expressions

Expressions can be evaluated, like:

  • Bindings
  • Functions
  • Special Forms

Bindings

A binding links a symbol to some data payload, and is created via the define special form:


(define a 'x)

Closures

Closures are functions together with an environment, and result from the lambda special form:


(define fun (lambda (arg1 arg2) (print arg1) (print arg2)))

Function Application

The first element of an unquoted list is interpreted as an expression that evaluates to a function and the remaining elements as arguments to that function:


(fun arg1 arg2)

The function evaluation is eager; so first, the argument expressions are evaluated, then the function application is using the evaluated arguments.

Arguments

Arguments are evaluated and passed as a list of values to a function.

This means the function parameters can be set up in various ways:


(lambda x ...)          ; x is a list
(lambda (x y) ...)      ; destructured list with elements x and y
(lambda (x y . z) ...)  ; z is a list (which is by default nil)

Optional arguments can be passed as a list, like z above.

Recursion

LISPs rely on recursion instead of iteration. Recursion refers to a function calling itself. Two features of SLED help avoid a stack overflow in deep recursions: the trampoline evaluator and tail-call optimization (TCO). TCO works for lambda , let , begin , if , ifnil , and apply . Additionally, TCO works for cons if the recursion runs in the second argument.

Errors

An error during evaluation of an expression jumps back to the top-level, where an error occurrence can be tested. An error cannot be caught inside an expression.

Builtin Functions

A set of functions is built into the SLED executable to enable interaction with the system and core functionality, for details see the index .

Standard Library

Beyond the core functions a set of typical functions is implemented as a standard library in the file sled.scm . For details see the index . The standard library may be extended with additional custom definitions.

Special Forms

Certain forms appear like functions but are not. These so-called special forms do not follow the function behavior, but use the same syntax as functions. For example, if does not evaluate its arguments before resolving the form. For details see the index .

Immutability

Special symbols, special forms, builtin functions, and standard library contents are immutable in SLED. Furthermore, special forms and builtin functions cannot be shadowed. Standard library and adjacent custom definitions cannot be redefined.

System

This LISP system is a DOS application.

File Names

File names should follow DOS 8.3 naming (maximum 8 characters for the file name, a dot, maximum 3 characters for the file extension). The recommended file extension for scripts running on this LISP is .scm due to syntactic similarity to Scheme ; for instance, the standard library is named sled.scm . However, the interpreter does not check the file extension.

Startup

The first action sled takes is loading its standard library, which has to have the name sled.scm and is expected in the same directory as the SLED.EXE interpreter executable. All symbols and their values loaded from the standard library become immutable.

Command-Line Arguments

The sled binary has four mutually exclusive command-line arguments. The first just displays a help page:


C:\> sled /?

The second is a path to a Lisp source file to be loaded before the REPL starts, but after the standard library loaded:


C:\> sled code.scm

The third is a "batch mode" switch /B , which exits after execution:


C:\> sled /B code.scm

The fourth is an "ignore errors" switch /I , which behaves like /B but continues execution after an error occurs:


C:\> sled /I code.scm

The file path has to be the last argument.

File Path

In the interpreter, if the path contains backslashes these need to be escaped, since the path becomes a symbol and the backslash \ alone is not an admissible symbol character.


C:\> sled to\my\code.scm


(load 'to\\my\\code.scm)

The file path also falls under the 16 character limit.

REPL

Once sled started, the read-eval-print-loop (REPL) begins with a prompt:


sled>

It reads input, evaluates it, prints the result, and prompts again.

Now have fun:


sled> (println 'hello _ 'world)

Extended Characters

A pitfall is extended (two-byte) characters which are not supported. An example is using the arrow keys in the REPL, resulting in an α (alpha) in the standard input echo. These extended characters pollute the input stream and can cause an error in an input line even if deleted.

Exiting

There are two regular ways to exit sled .

The first is the dollar symbol $ on the top-level, which tells the parser to exit:


sled> $

The second is the (exit) builtin function, which upon evaluation exits:


(exit)

Comments

Comments are ignored by the parser. A comment is introduced by a semicolon:


;

All characters until the next line break are ignored by the parser. Traditionally, the number of consecutive semicolons conveys semantics, similar to Markdown headings:


;;;; main title

;;; section title

;; start of line

; end of line

Furthermore, block comments are realized via a special form named comment :


(comment ...)

Parentheses inside a comment form need to be balanced:


(comment ())  ; OK
(comment ()   ; → error

Note that a comment form cannot be quoted:


'(comment test)  ; → error

Breaking

To break pure computation, use CTRL+Break , for breaking input CTRL+C is available.

Help

In the REPL, the symbol ? can be used to list special forms, builtin functions, and standard library symbols.

Limits

As real-mode DOS program, SLED has multiple constraints:

  • The heap has 12288 nodes
  • The symbol table has 2048 characters

The standard library consumes about 5% of nodes and characters.

Index

Special Symbols Special Forms Builtin Functions
apply atom? cons
defined? empty? env
eof? equiv? error
exit gc head
load newline print
proc? read restart
symbol? tail value
Standard Aliases Standard Library
and? append assert
compose dec equal?
error? get id
inc list list?
map member or?
pair? printid println
put reverse shorter?

$ {exit}

This special symbol exits the REPL. This is not a short form of (exit) , as it is resolved by the parser. Works only from the prompt.


? {help}

This special symbol holds an overview of special forms, builtin functions, and the standard library. Works only from the prompt.


' {quote}

This special symbol is an alias for the quote special form.


_ {space}

This standard alias is for the space character '\ .


and? <arg1> <arg2>

This standard library binary predicate answers if both arguments are not nil ; use to make compound conditionals simpler.


(and? nil nil)    ; → nil
(and? nil true)   ; → nil
(and? true nil)   ; → nil
(and? true true)  ; → true

NOTE: Unlike in Scheme, this is not a special form of variadic arguments which are evaluated sequentially until a false result, but a binary function that evaluates both arguments.

ans

This special symbol contains the result of the last top-level form that produced a value; in case of an error, the err symbol is set, which can be tested for with error? .


ans


append <lst1> <lst2>

This standard library binary function returns a list consisting of the second argument list concatenated to the end of the first argument list.


(append nil (list 'a))        ; → (a)
(append (list 'a) nil)        ; → (a)
(append (list 'a) (list 'b))  ; → (a b)
(append '(a b) '(c d))        ; → (a b c d)
(append '(a) 'b)              ; → (a . b)


apply <fun> <lst>

This builtin binary function evaluates the first argument function with the second argument list as arguments.


(apply list '(a b c))  ; → (a b c)


assert <arg> <sym>

This standard library binary procedure prints the second argument and causes an error, if the first argument evaluates to nil .


(assert nil 'list\ empty)


atom? <arg>

This builtin unary predicate answers if the argument is an atom.


(atom? nil)   ; → true
(atom? true)  ; → true
(atom? '(a))  ; → nil


begin <body1> ... <bodyN>

This special form evaluates its arguments in sequence and returns the last argument's return value.


(begin (print 'a) (print 'b) 'c)  ; → c
(begin)                           ; → nil


br

This standard alias is for newline .


This special form is not evaluated. Use as block comment. Unlike the other special forms, this is decoded by the parser before evaluation.


(comment this :-D is ignored)

NOTE: Parentheses inside comments have to be balanced.

compose <arg> <fun1> ... <funN>

This standard library variadic function pipelines unary functions: the second argument is applied to the first argument, the third argument is then applied to the previous return value and so on; the final argument's return value is returned as result.


(compose nil inc inc)  ; → (nil nil)


cons <arg1> <arg2>

This builtin binary function returns a pair with the first argument as head and second argument as tail.


(cons 'a 'b)  ; → (a . b)


dec <arg>

This standard library unary function returns the tail of a list if not empty; use to decrement von tally numbers.


(dec '(nil . nil))  ; → nil


define <sym> <arg>

This special form creates a new binding of the second argument to the first argument symbol, and returns the second argument. Bindings can be re- defined , except special symbols, special forms, builtin functions, or standard library symbols.


(define hello 'world)  ; → world

NOTE: define always affects the global binding, also when used inside let or lambda .

defined? <sym>

This builtin unary predicate answers if the argument symbol is already defined at global scope.


(defined? 'defined?)   ; → true
(defined? 'undefined)  ; → nil


empty? <arg>

This builtin unary predicate answers if its argument is the empty list.


(empty? nil)  ; → true


env

This builtin thunk prints the current user-defined symbols.


(env)


eof? <arg>

This builtin unary predicate answers if its argument evaluates to an end-of-file (EOF) or end-of-transmission (EOT) symbol.


(ifnil (eof? (read)) 'none)


equal? <arg1> <arg2>

This standard library binary predicate answers if the arguments are recursively equal. Use to compare pairs and lists, however equal? falls back to equiv? for atoms.


(equal? '(nil nil) (list nil nil))  ; → true
(equal? nil nil)                    ; → true


equiv? <arg1> <arg2>

This builtin binary predicate answers if the arguments are shallowly equal. Use to compare atoms.


(equiv? nil nil)  ; → true


err

This special symbol marks an error state.


(head nil) (equiv? err ans)  ; → true


error <sym> [<arg>]

This builtin procedure throws an error and thus causes a break in evaluation of the current form. Furthermore, the first argument symbol (error message) and the optional second argument expression (error reason) are printed.


(error 'bad\ error (list 'not 'right))


error?

This standard library thunk predicate answers if the previous evaluation resulted in an error.


(head nil) (error?)  ; → true


exit

This builtin thunk quits the interpreter or REPL.


(exit)  ; back to DOS


gc [<arg>]

This builtin procedure triggers garbage collection. Node usage is printed if an argument is provided which is not nil .


(gc)       ; → *no output*
(gc nil)   ; → *no output*
(gc true)  ; → *prints node usage*


get <sym> <lst>

This standard library binary function returns the value paired to the first argument symbol if found in the second argument association list, or nil otherwise.


(get 'a '((a . x) (b . y)))  ; → x
(get 'z '((a . x) (b . y)))  ; → nil

NOTE: Uses equiv? for key comparisons.

This builtin unary function returns the head part of a cons cell or list.


(head (cons 'a 'b))  ; → a

NOTE: This function corresponds to car in classic LISP and Scheme.

id <arg>

This standard library unary function returns its argument. Use as identity function.


(id 'x)  ; → x


if <arg1> <arg2> [<arg3>]

This special form evaluates the first argument; if it does not evaluate to nil , the second argument is evaluated and returned, otherwise the third argument is evaluated and returned, or `nil` if no third argument is given.


(if 'ok 'con 'alt)  ; → con
(if nil 'con 'alt)  ; → alt
(if 'ok 'con)       ; → con
(if nil 'con)       ; →


ifnil <arg1> <arg2>

This special form evaluates the first argument and returns its result if it is not nil , otherwise the second argument is evaluated and returned.


(ifnil 'ok 'alt)  ; → ok
(ifnil nil 'alt)  ; → alt


inc <arg>

This standard library unary function prepends nil to a list. Use to increment tally numbers.


(inc nil)     ; → (nil)
(inc '(nil))  ; → (nil nil)


lambda (<arg1> ... <argN>) <body1> ... <bodyN>

This special form creates a function with arguments as destructured list and a sequentially evaluated body, whose last expression is the return value.


(lambda (x y) (print x) y)  ; → function that prints x and returns y


let (<arg1> <arg2>) <body1> ... <bodyN>

This special form creates a scope with a local binding and evaluates its body sequentially.


(let (x 'y) x)  ; → y

NOTE: Unlike Scheme, this special form allows only a single local binding. Multiple bindings can be realized by nesting let s.

list <arg1> ... <argN>

This standard library variadic function constructs a list of its arguments.


(list)        ; → nil
(list 'a)     ; → (a)
(list 'a 'b)  ; → (a b)


list? <arg>

This standard library unary predicate answers if the argument is a proper list.


(list? nil)     ; → true
(list? true)    ; → nil
(list? '(x y))  ; → true


load <sym> [<arg>]

This builtin function evaluates the contents of the file given by the argument symbol (path) and returns the last answer. If a second argument, which is not nil , is given then errors are ignored during loading.


(load 'myscript.scm)
(load 'myscript.scm true)

NOTE: Loads can be nested twice at most.

map <fun> <lst>

This standard library binary function applies the first argument unary function to each element of the second argument list and returns the list of return values.


(map inc '(nil (nil)))  ; → ((nil) (nil nil))


member <arg> <lst>

This standard library binary function returns the pair from the second argument list whose head is the first argument; otherwise nil is returned.


(member 'b '(a b c))  ; → (b c)
(member 'd '(a b c))  ; → nil

NOTE: Uses equiv? for list element comparisons.

newline

This builtin thunk prints a line break; use for output formatting.


(newline)


nil

This special symbol represents the empty list; and is also the only value evaluating to "false". Equivalently, '() can be used for nil .


nil  ; → nil
'()  ; → nil


nil? <arg>

This is a standard alias for empty? ; use as test for nil .


(nil? nil)     ; → true
(nil? '())     ; → true
(nil? _)       ; → nil
(nil? (list))  ; → true


not <arg>

This is a standard alias for empty? ; use for inverting predicate results.


(not nil)   ; → true
(not true)  ; → nil


or? <arg1> <arg2>

This standard library binary predicate answers if any argument is not nil ; use to simplify compound conditionals.


(or? nil nil)    ; → nil
(or? nil true)   ; → true
(or? true nil)   ; → true
(or? true true)  ; → true

NOTE: Unlike in Scheme, this is not a special form of variadic arguments which are evaluated sequentially until a true result, but a binary function that evaluates both arguments.

pair? <arg>

This standard library unary predicate answers if the argument is not an atom.


(pair? 'a)      ; → nil
(pair? '(a b))  ; → true
(pair? nil)     ; → nil


print <arg1> ... <argN>

This builtin variadic procedure prints its arguments to the standard output.


(print 'hi)
(print 'hello _ 'world)
(print '(a b))


printid <arg1> [<arg2>]

This standard library function returns the first argument after printing it and a line break; if given, the second argument is printed before the first.


(printid ans 'answer)


println <arg1> ... <argN>

This standard library variadic procedure prints its arguments to the standard output and appends a line break.


(println 'hi)
(println 'hello _ 'world)
(println '(a b))


proc? <arg>

This builtin unary predicate answers if its argument is a closure or builtin function. For special forms, this predicate returns nil .


(proc? map)    ; → true
(proc? proc?)  ; → true
(proc? nil)    ; → nil
(proc? if)     ; → nil


put <sym> <arg> <lst>

This standard library function returns an updated third argument association list by setting the tail of the pair with the first argument symbol as head, or adding a pair with the first argument symbol as head and the second argument as tail, if no pair with the first argument symbol as head is listed in the third argument.


(put 'hello 'world nil)               ; → ((hello . world))
(put 'one 'uno '((one . a)))          ; → ((one . uno))
(put 'two 'b '((one . a)))            ; → ((one . a) (two . b))
(put 'two 'z '((one . a) (two . b)))  ; → ((one . a) (two . z))


quit

This standard alias is for exit .


quote <arg>

This special form returns its argument unevaluated.


(quote a)  ; → a
'a         ; → a


read

This builtin thunk returns a symbol read as a line of input from the standard input source terminated by a line break via return key / enter key.


(read)


restart [<sym>]

This builtin function resets and restarts the interpreter and optionally loads a file specified by the symbol argument. All definitions are lost!


(restart 'next.scm)


reverse <lst>

This standard library unary function reverses its list argument.


(reverse (list 'a 'b 'c))  ; → (c b a)


self <arg1> ... <argN>

This special symbol enables anonymous recursion. Inside any closure it resolves to the enclosing lambda . Outside a lambda , self is undefined.


((lambda (x)
  (if (empty? x) nil
                 (self (tail x)))) (list nil nil))  ; → nil


shorter? <lst1> <lst2>

This standard library binary predicate answers if the first argument list has less elements than the second argument list.


(shorter? nil (list nil))  ; → true
(shorter? (list nil) nil)  ; → nil


symbol? <arg>

This builtin unary predicate answers if its argument is a symbol.


(symbol? 'a)  ; → true


tail <arg>

This builtin unary function returns the tail part of a cons cell.


(tail (cons 'a 'b))  ; → b

NOTE: This function corresponds to cdr in classic LISP and Scheme.

true

This special symbol evaluates to true. Use as a generic "true" value.


true  ; → true


value <sym>

This builtin unary function resolves the value of its symbol argument. Respects lexical scope.


(value 'ver)  ; → sled-0.4


ver

This special symbol evaluates to a symbol pinpointing the version of SLED.


ver  ; → sled-0.4


zero? <arg>

This is a standard alias for empty? ; use for testing tally numbers.


(zero? nil)     ; → true
(zero? '(nil))  ; → nil

Usage

  • SLED is made for a disk operating system like FreeDOS or MS-DOS
  • Outside DOS, a DOS emulator like DOSBox , DOSBox-X , DOSBox-Staging is required
  • Run the shell script ./sled.sh (auto-selects the installed DOSBox) on Linux, BSD, MacOS, or Unix
  • Building SLED requires Microsoft C Compiler or Open Watcom , as well as make
  • Build with MS C 6.0A: make build_msc (Compiler location via MSC )
  • Build with Open Watcom v2: make build_owc (Compiler location via OWC )
  • Run build: make run
  • Run tests: make tests
  • Run benchmark: make bench (Takeuchi function, see this and that )

This project by gramian is licensed under the 0BSD (Zero-Clause BSD) license.

2026 Hugo Awards

Hacker News
www.thehugoawards.org
2026-09-06 08:41:59
Comments...
Original Article

LAcon V, the 84th World Science Fiction Convention, presented the 2026 Hugo Awards in a ceremony held at the convention on Sunday, August 30th, 2026. The full results including the detailed reports on voting are published on the LACon V website . The results will be updated here on The Hugo Awards site as soon as our bandwidth allows.

Best Novel
The Everlasting by Alix E. Harrow (Tor US; Tor UK)

Best Novella
The River Has Roots by Amal El-Mohtar (Tordotcom; Arcadia UK)

Best Novelette
“Never Eaten Vegetables” by H.H. Pak (Clarkesworld, Issue 220)

Best Short Story
“In My Country” by Thomas Ha (Clarkesworld, Issue 223)

Best Series
Old Man’s War by John Scalzi (Tor US; Tor UK)

Best Graphic Story or Comic
A Wizard of Earthsea: A Graphic Novel, written by Ursula K. Le Guin, adapted and art by Fred Fordham (Clarion Books; Walker UK)

Best Related Work
Inventing the Renaissance by Ada Palmer (University of Chicago Press US, Head of Zeus UK)

Best Dramatic Presentation, Long Form
Sinners, screenplay by Ryan Coogler, directed by Ryan Coogler (Proximity Media, Warner Bros. Pictures)

Best Dramatic Presentation, Short Form
Murderbot: “All Systems Red”, written by Paul Weitz & Chris Weitz, directed by Roseanne Liang, based on the book All Systems Red by Martha Wells (Apple TV)

Best Game or Interactive Work
Clair Obscur: Expedition 33, developed by Sandfall Interactive, published by Kepler Interactive

Best Editor Short Form
Neil Clarke

Best Editor Long Form
Diana M. Pho

Best Professional Artist
John Picacio

Best Semiprozine
Uncanny Magazine, publisher and editor-in-chief: Michael Damian Thomas; managing editor Monte Lin; poetry editor Betsy Aoki, podcast producers Erika Ensign and Steven Schapansky

Best Fanzine
nerds of a feather, flock together, editors Roseanna Pendlebury, Arturo Serrano, Paul Weimer; senior editors Joe Sherry, G. Brown, Vance Kotrla

Best Fancast
Hugo, Girl!, presented by Haley Zapal, Amy Salley, Lori Anderson, and Kevin Anderson

Best Fan Writer
Jason Sanford

Best Fan Artist
Yuumei

Best Poem
“Hex Supply Customer Support Log” by Elis Montgomery (Strange Horizons, Issue 25 August 2025)

Lodestar Award for Best YA Book
Coffeeshop in an Alternate Universe by C.B. Lee (Feiwel & Friends)

Astounding Award for Best New Writer (sponsored by Must Read Books Publishing)
Antonia Hodgson (1st year of eligibility)

LA con V, the 2026 Worldcon, announced on June 15, 2026, that the 2026 Hugo Award trophy base will be designed by Scott Lefton, while the 2026 Lodestar Award will be designed by H Emiko Ogasawara.

Each year’s Hugo Award trophy consists of the rocket originally designed by Jack McKnight and Ben Jason and later refined to its current form by Peter Weston, mounted on a base designed specified by each year’s Worldcon committee.

The design of each year’s Lodestar Award is specified by that year’s Worldcon committee.

The final base designs will be revealed as part of the convention’s Opening Ceremony in Anaheim on August 27, 2026.

For more information, see the full announcement at the LAcon V website .

This is from an announcement sent to LAcon V members by Hugo Award Administrator Tammy Coxen:


Hugo Award voting is now open! Anyone who is an Attending, Online (Virtual), or World Science Fiction Society (WSFS) member of LAcon V prior to the close of voting is eligible to vote in the 2026 Hugo Awards.

Votes can be submitted through our online balloting system, NomNom , or by mailing in a printable ballot ( available here ). Voting will close on August 8, 2026 at noon PDT.

You can also use NomNom to download the Hugo Awards Voter Packet. We are grateful to the rights holders who generously donate works to the Packet. This resource is provided to allow Hugo Award voters to make informed decisions. Access to the Packet ends when voting closes.

In order to use NomNom you will first need to create an LAcon V virtual convention account. LAcon V members who nominated should already have an account set up. If you joined too late to nominate you will have received an email with instructions on how to set up your account .

Please note that if you made nominations using your Seattle Worldcon 2025 membership, you will not be able to use that account to vote and will need to set up and use your LAcon account instead.

If you use NomNom, you can edit your ballot as often as you like until the close of the voting period. If you have already set up your virtual convention account you can access the ballot directly by logging in to NomNom .

If you encounter any issues or have questions about the Hugo Awards, please contact hugo-help@lacon.org for assistance.

Hugo Award Logo without text and in pure black and white
Hugo Award Logo

LAcon V, the 84th World Science Fiction Convention , announced on April 21, 2026 the finalists for the 2026 Hugo Awards, Lodestar Award for Best Young Adult Book, and Astounding Award for Best New Writer. The full list of finalists is on the 2026 Hugo Awards page .

“The 2026 Hugo Award ballot reflects the wide spectrum of contemporary science fiction and fantasy,” said convention chair Joyce Lloyd. “This shortlist highlights established voices and emerging creators. These are works that comfort us in hard times and challenge us to not only envision, but to work towards, better tomorrows and more inclusive worlds.”

All members of the World Science Fiction Convention are able to vote on the Hugo Awards, including those who have memberships in the virtual portion of the convention. Voting on the final ballot will begin in early May. Information about registration is available at lacon.org/register . In addition to the traditional categories, the 2026 awards will again include a Best Poem category, following a trial run in 2025.

“There was a lot of enthusiasm for the Best Poem category in Seattle,” Hugo Awards Administrator Tammy Coxen said. “We felt that running the category for an additional year would help provide data about its long-term viability before the business meeting chooses whether or not to ratify the amendment and add Best Poem to the Hugo Awards as a permanent category.”

LAcon V, the 2026 World Science Fiction Convention (Worldcon), is presented by the Southern California Institute for Fan Interests (SCIFI), Inc., a California 501(c)(3) not-for-profit organization.

The members of the 2025 and 2026 World Science Fiction Conventions cast 1,488 valid nominating ballots, including 6 paper ballots, for the 2026 Hugo Awards. The nominators made 23,543 nominations for 4,299 works and individuals across 21 categories.

Voting on the final ballot will open in early May 2026. Only LAcon V WSFS members will be able to vote on the final ballot and choose the winners for the 2026 Awards.

The 2026 Hugo Awards, the Lodestar Award, and the Astounding Award will be presented on Sunday evening, August 30, 2025 at a formal ceremony at LAcon V.

Questions about the Hugo Awards process should be directed to hugo-help@lacon.org . Please do not submit questions to the Hugo Awards website, as we do not administer the Hugo Awards and can only pass your questions on to the award administrators.

Nominations are now open for the 2026 Hugo Awards. See the LACon V Worldcon website for nomination information.

WSFS members of the 2026 Worldcon (LACon V) and 2025 Seattle Worldcon as of January 31, 2026 are eligible to make up to five equally weighted nominations in each category of the Hugo Awards, as well as the Lodestar Award for best Young Adult Book and the Astounding Award for Best New Writer. You do not need to nominate in all categories, nor do you need to make the maximum number of nominations in any or all categories. Those works/people who receive sufficient nominations will be go on to the final ballot, where WSFS members of LACon V will vote among them. The winners will be announced at a ceremony at LACon V, the 2026 Worldcon.

All ballots must be received by Saturday, March 28, 2026, 12:00 (Noon) Pacific Daylight Time (UTC-7).

The 2026 Hugo Awards are administered by LACon V, not by the Hugo Awards web site. Address any questions about the 2026 Hugo Awards to the Hugo Awards Administrators of LAcon V at hugo-help@lacon.org .

LAcon V, the 2026 Worldcon, announced on December 1, 2025 that they would use their authority under Section 3.3.20 of the WSFS Constitution to present a Hugo Award for Best Poem as part of the 2026 Hugo Awards.

The 2025 Worldcon in Seattle also presented a Best Poem category. There is a proposal to add Best Poem as a permanent category that will be up for ratification at the 2026 WSFS Business Meeting. As stated in LAcon V’s announcement:

“There was a lot of enthusiasm for the Best Poem category in Seattle,” Hugo Awards administrator Tammy Coxen said. “We felt that running the category for an additional year would help provide data about its long-term viability before the business meeting chooses whether or not to ratify the amendment and add Best Poem to The Hugo Awards as a permanent category.”

Best Poem will be on the nominating ballot for the 2026 Hugo Awards along with the permanent categories, the Lodestar Award for Best Young Adult Book, and the Astounding Award for Best New Writer. All WSFS members of the 2025 and 2026 Worldcon as of the end of January 2026 will be eligible to nominate works for the 2026 Awards.

For more information, see the full announcement at the LAcon V website .

The winners of the 2025 Hugo Awards, Astounding Award for Best New Writer, and Lodestar Award for Best Young Adult Book were announced and the awards presented at a ceremony at Seattle Worldcon 2025, the 83rd World Science Fiction Convention, on August 16, 2025 at the Seattle Convention Center. The winners, the Hugo Award administrators’ report, and the breakdown of the first place voting on each category on the final ballot are available at the Seattle Worldcon 2025 website .

Voting opened on April 23, 2025 for the final ballot of the 2025 Hugo Awards, Lodestar Award for Best Young Adult Book, and the Astounding Award for Best New Writer. All ballots must be received by Wednesday, July 23 rd , 2025, 23:59 PDT .

Continue reading

Hugo Award Logo without text and in pure black and white
Hugo Award Logo

Seattle Worldcon 2025, the 83rd World Science Fiction Convention , announced on April 6, 2025 the finalists for the 2025 Hugo Awards, Lodestar Award for Best Young Adult Book, and Astounding Award for Best New Writer. The full list of finalists is on the 2025 Hugo Awards page .

1,338 valid electronic nominating ballots were received by the deadline of March 14 at 11:59 p.m. PDT and counted from the members of the 2024 and 2025 World Science Fiction Conventions for the 2025 Hugo Awards. ​​Unfortunately, two mailed ballots were received 2.5 weeks later on April 3 after the deadline of receipt. (The initial publication of the finalists had an error of 1,738 ballots instead of the correct number of 1,338.)

Voting on the final ballot will open during April 2025.

Only Seattle Worldcon 2025 WSFS members will be able to vote on the final ballot and choose the winners for the 2025 Awards. The 2025 Hugo Awards, the Lodestar Award, and the Astounding Award will be presented on Saturday evening, August 16, 2025, at a formal ceremony at Seattle Worldcon 2025.

Questions about the Hugo Awards process should be directed to hugo-help@seattlein2025.org . Please do not submit questions to the Hugo Awards website, as we do not administer the Hugo Awards and can only pass your questions on to the award administrators.

Nominations are now open for the 2025 Hugo Awards. See the 2025 Seattle Worldcon website for nomination informatio n.

WSFS members of the 2025 Seattle Worldcon and 2024 Glasgow Worldcon are eligible to make up to five equally weighted nominations in each category of the Hugo Awards, as well as the Lodestar Award for best Young Adult Book and the Astounding Award for Best New Writer. You do not need to nominate in all categories, nor do you need to make the maximum number of nominations in any or all categories. Those works/people who receive sufficient nominations will be go on to the final ballot, where WSFS members of Seattle Worldcon 2025 will vote among them. The winners will be announced at a ceremony at Seattle Worldcon 2025.

All ballots must be received by March 14, 2025, 23:59 (11:59 p.m.) Pacific Daylight Time (UTC-7).

The 2025 Hugo Awards are administered by Seattle Worldcon 2025, not by the Hugo Awards web site. Address any questions about the 2025 Hugo Awards to Seattle Worldcon 2025.

Who said a tech addict can't be comfortable far from home?

Lobsters
a.l3x.in
2026-09-06 08:41:25
Comments...
Original Article

As far as my memory goes back I always loved to travel. In the last ~30 years I had the privilege to go through my fair share of intercontinental flights, changes of accommodation, and lived in different foreign countries for various years.

During this time I inevitably developed (and constantly refine) my own ideas of what comfort really is, built techniques and gathered technologies to try to recreate that sensation, that warm fuzzy feeling I usually get while laying on my real home’s 1 couch, wherever I find myself to be.

Recently, after a relatively long while spent living surrounded by the many comforts of my own house in the Dominican Republic, I decided to visit South East Asia once more 2 and I’m just now realising that I’m quite satisfied with this simple (ok, maybe not that simple) geeky setup I came up with.

In this article I’ll try to describe it in some details in the hope it might give you some good ideas too and, even better, that I might receive new interesting ones as feedback. See the /contact page for how to get in touch with me.

Comfortably dumb

Ideally I’d like to always be able to walk into any new house/apartment/hotel room I go by and, if I stay for more than just a few days, have all the digital goodies I’m used to there for me, preferably without having to setup a new (WiFi) connection for each one of the devices I own.

More precisely, I want to be able to automatically sync my files locally with Resilio Sync 3 , watch the occasional YouTube video, a movie or TV show episode streamed by my Plex Server, play a videogame on a TV screen, or listen to music and podcasts from various steaming services, all reproduced with decent audio fidelity 4 .

Assumptions I make

  • I’ll have some way to connect to the public internet, either via Ethernet/Wifi lan provided by the place or some cellular 4-5G/LTE kind of (paid) service. Unless I’m planning to spend some days camping, on a sailing cruise or in some very remote area, that’s practically true all the time
  • there’ll be a TV or a monitor with at least one HDMI port. Usually there is one almost everywhere I go nowadays, in the worst case scenario I might decide to buy a second hand (or even new, they come as cheap as ~100USD) one if I plan to stay for a long enough period of time
  • 110/220V A/C power outlets 5

The Gear

This is an exhaustive (and admittedly a little exhausting to read) list of all the gear I have in my pockets or inside a luggage or backpack when I travel 6 :

Macbook and iPhone are by far the most precious (and expensive) devices I own. I think I could live without all the other pieces (albeit missing them dearly, of course) for a reasonably long amount of time. I can’t think of spending even a single day without Macbook and iPhone at reach anymore.

I’m supposed to be talking about comfort though so… let me show you how I actually make use of the listed gear to reach that sweet nerdy spot.

Connect all the things 🔌

The following Mermaid flowchart represents more or less 8 how all the devices are interconnected:

flowchart TD
    SW((Ethernet Switch))

    MR[GL-AR300M]
    MODEM[4G/LTE Modem]
    SONOSPORT[Sonos Port]
    TV[TV]
    SERVER[Server]
    NSWD[Nintendo Switch Dock]
    NSW[Nintendo Switch]
    ANKER[Docking Station]
    MBP[MacBook]
    MINIRIG[Minirig 🔊]
    ROAM[Sonos Roam 🔊]
    IPHONE[iPhone]
    STICK[TV Stick]
    KINDLE[Kindle]

    MODEM <-->|USB| MR
    MR <-->|LAN| SW

    SW <-->|LAN| SONOSPORT
    TV -->|mini jack| SONOSPORT
    SONOSPORT -->|mini jack| MINIRIG

    SW <-->|LAN| SERVER

    SW <-->|LAN| NSWD
    NSWD -->|HDMI| TV
    STICK -->|HDMI| TV

    SW <-->|LAN| ANKER
    ANKER <==>|USB-C 🔌| MBP

    MR <-.->|Wi-Fi| ROAM
    MR <-.->|Wi-Fi| IPHONE
    MR <-.->|Wi-Fi| KINDLE
    MR <-.->|Wi-Fi| NSW

It can actually get a little more complicated then that if I drop the Aveek audio mixer into the picture, usually between the final speaker (e.g. Minirig) and other sources like Sonos Port, the Anker dock’s sound card, the Reloop controller, and/or whatever else that could be plugged into a (mini) jack stereo port. Let’s just say this is the canonical setup that I usually settle for as a baseline.

Side note: Sonos devices connected to a single system also join their own proprietary mesh network ( SonosNet ), adding that to the chart just seems to add confusion, I mention it here for completeness sake.

Home is where WiFi auto joins

Despite its size the delightfully small GL-AR300M mini router (which I map in my /etc/hosts as minirouter for easy access) is probably the most important piece of all. It’s an OpenWRT-based network router which gives me all the possible flexibility I could ask for when travelling.

To prove the point, once I got a hold on some kind of internet connection, I generally enter the new place and:

  1. power minirouter up and wait a couple of minutes for it to boot
  2. log into its admin web page and setup the WAN interface: if I have a wifi password I set it in repeater mode, if all I have is a mobile data plan I plug the LTE modem with the sim card in. If I’m in a rush I might just plug the iPhone in with an USB-A to USB-C cable and set the WAN router’s interface in tethering mode

That’s it! What takes longer is perhaps deciding where to place the device, the actual router setup is ~5 minutes, to be conservative.

Audio routing

Perhaps you’ve been wondering: why the Sonos Port? is the component which looks the most at odds in a travel setup but I’ll make here my stand: it’s awesome!

Essentially it works as a flexible audio router and lets me dynamically define all sorts of input/output setups so I can for example:

  • listen to what the TV stick or the docked Switch is reproducing on both the Roam and the Minirig speakers simultaneously
  • let a YT music podcast or a YT video concert play out on the Minirig, have the Roam come with me in the bathroom while I shower and listen to something else entirely
  • plug in the DJing console as input and play a live set using both Roam and Minirig (or whatever other speaker I might have at hand) as speakers
  • plug the laptop / docking station audio interface as input and use the speakers as output when e.g. I want to play music / recorded live sets from my hard drive
  • setup a timer and let the music (or a sleeping inducing story) to turn off automatically
  • normally I let myself wake up naturally but when I need to e.g. catch an early flight I find that waking up with (low but not too low) music is a somehow less unpleasant experience
  • any Sonos system is easily extensible, so far I’m more than happy with what I have but adding more speakers in case is going to be trivial 9

Wait but why?

The fact I can now use my LAN has various benefits beside that I don’t need to type copy&paste passwords again and again. Perhaps most notably I can now connect seamlessly to my Sonos devices and the Amazon TV stick is ready with my Youtube Premium, Twitch and Plex TV accounts to stream media from my Beelink server. I also like to have my Resilio Sync main storage close by so not to have to transfer data outside the LAN for quickly backing up all my files including new photos, videos and what not.

Another perk is that I can use Wireguard 10 out of the box to e.g. open an ssh session into the Beelink server without having to do anything specific, just turn on the Wireguard client on my carry-on device (iPhone/Macbook) when I’m not inside the LAN and target the private hostname. For the records, it works because the Beelink server connects at boot to an external Wireguard instance that acts as the central hub (star topology) and knows how to route network packets to private interfaces. To make it work I had to add a single persistent SNAT firewall rule for the GL-AR300M but this is out of scope for this article and I may decide to blog about that in the future 11 .

There’s also a non-tech practical reason to bring it all with me: if I won’t take this stuff with me it means I’ll have to leave it… somewhere , either at home (which I might want to rent out), drop at some friend’s place, sell or give away, and perhaps have to buy at least some of it down the road yet one more time. I realized that it’s just easier for me to take literally every piece of gear I own with me and forget that there might be other options. I’m a computer nerd after all, remember? I don’t own that many cloths anyway, there’s usually plenty of room in the (big) luggage 🤓.

Wrapping up

And… the final and apparently necessary disclaimer: I did ask my coding agent to write the initial draft of the Mermaid chart for me, because I’m lazy and, most notably, free inference 12 is just too tempting not to be used. That said, I actually wrote every other single word in this article personally, manually typing them, without asking for help nor advise to anyone, LLM or humans alike. I don’t know if we’ll ever figure out a way to prove it so for now you gotta trust me on this, I guess.

And that’s really all I had to share for today. Stay geeky, folks! ✌️

  1. Whatever that really means is still a mystery to me

  2. In Hua Hin, Thailand, while writing this piece. Last time in SEA was almost exactly 10 years ago… time really flies

  3. Kind of like Dropbox, but self hosted: https://www.resilio.com/sync/

  4. That is: no TV nor laptop speakers involved, please do that to yourself too and never ever use these noise generators, at least plug in some bluetooth headphones instead…

  5. the more the merrier but always better to take a multiplier and an universal adapter

  6. I always put all devices with batteries in the carry-on trolley or backpack that I take into the cabin whenever I fly. I strongly recommend you to always do the same

  7. Apparently the Roam v1 is not in production anymore, they look exactly the same to me though

  8. Glossing over a few components like the TV stick remote control, Switch pro controller, audio mixer, keyboard, touchpad, etc.

  9. I confess I’d love to be able to replace the Minirig with some kind of portable soundbar that connects to the TV via Arc … a device that Sonos will never produce, unfortunately. Oh well, a tech addict gotta dream

  10. I hear good things about Tailscale services but I still prefer to setup my own networks

  11. Or not, given that there are plenty of well made tutorials for how to setup Wireguard successfully

  12. Today there are many options to obtain LLM inference for free, e.g. https://opencode.ai/go and https://openrouter.ai/ both offer free models regularly

The pencil case model of creativity

Hacker News
dub.uu.nl
2026-09-06 08:05:34
Comments...
Original Article

I delete the same sentence from my thesis introduction, for the twentieth time today. It feels like no combination of letters will save me: how will any of them convince a PhD assessment committee?

The ringing bell is a welcome interruption. I join my friend for a walk, happy to enjoy the three days of summer we get this year.

We wander through the canals when she stops abruptly, summons a notebook, and starts sketching the alleyways. She tells me how she can use it in her next commission: a cover for a fantasy book.

For a few minutes, her attention is entirely absorbed. She sees something I am not, hears magic whispers in an unknown language, and takes notes of their message.

“I envy how creative your job is,” I tell her.

“But your job is creative too, no?”

Not the right question for today.

“Well, in a way,” I say. “I can be creative with my lectures and my research, but both come with constraints. I teach what needs to be taught. I research what I'm funded to research. My work depends on collaborators, deadlines, publishing venues... It never feels as creative as stopping in the middle of the street because you've just found inspiration.”

We stay silent for a while.

“You know, I was quite creative as a kid,” I add. “Always making up stories. My mum could leave me with a pencil case and come back hours later, to hear all about the adventures of the courageous Blue Pen, who fought the evil Black Pencil for the love of Red Eraser.”

“Look, I get it,” she laughs. “But you see: I have a creative job. I’ll stop mid-street if I get an idea, but I also have to prepare the content they asked for. I can't use just any colour because it depends on the printer. It depends entirely on what the client wants, and there is nothing - nothing - that will make preparing the printing tests any less boring.”

She shrugs.

“It's on you to find space for creativity. If you can't find it in your job, no one will.”

She continues towards a café, sketchbook in her hand. In my desk drawer, I have a notebook of the same size. It is filled with spiderwebs of connections between concepts from the papers I read.

I think about my supervisors and me, surrounded by post-it notes, trying to find themes amid participants’ responses, suddenly realising two themes belong together. The whiteboard behind my desk, covered with Dutch expressions, plant-watering duties, and the structure of my PhD thesis.

Somewhere along the way, I stopped recognising the creativity in what I was doing. I told myself that creativity required a freedom I did not have. But maybe creativity exists precisely within boundaries, because they define the space in which our ideas can play with one another.

After all, if I once built endless stories from a pencil case, surely a PhD project is enough to work with.

On Staff's Viewpoint , UU's employees share their views with the UU community. Isabela Saccardi, a PhD candidate in the Human-Centred Computing Group, is one of the columnists invited to write in this space. You can also read students' perspectives on Students' Viewpoint and click here to check out students' and staff's columns in Dutch.

Intellectual Fly Is Open

Hacker News
bcantrill.dtrace.org
2026-09-06 07:56:43
Comments...
Original Article

Note: This was originally published as a LinkedIn post on November 11, 2025.

I need to make a painful confession: somehow, LinkedIn has become an important social network to me. This isn’t necessarily due to LinkedIn’s sparkling competence, of course. To the contrary, LinkedIn is the Gerald Ford of social networks: the normal one that was left standing as the Richard Nixons and the Spiro Agnews of social media imploded around them. As with Gerald Ford, with LinkedIn we know that we’re getting something a bit clumsy and boring, but (as with post-Watergate America!), we’re also getting something that isn’t totally crooked — and that’s actually a bit of a relief.

But because I am finding I am spending more time here, we need to have some real talk: too many of you are using LLMs to generate content. Now, this isn’t entirely your fault: as if LLMs weren’t tempting enough, LinkedIn itself is cheerfully (insistently!) offering to help you "rewrite it with AI." It seems so excited to help you out, why not let it chip in and ease your own burden?

Because holy hell, the writing sucks. It’s not that it’s mediocre (though certainly that!), it’s that it is so stylistically grating, riddled with emojis and single-sentence paragraphs and "it’s not just…​ but also" constructions and (yes!) em-dashes that some of us use naturally — but most don’t (or shouldn’t).

When you use an LLM to author a post, you may think you are generating plausible writing, but you aren’t: to anyone who has seen even a modicum of LLM-generated content (a rapidly expanding demographic!), the LLM tells are impossible to ignore. Bluntly, your intellectual fly is open: lots of people notice — but no one is pointing it out. And the problem isn’t merely embarrassment: when you — person whose perspective I want to hear! — are obviously using an LLM to write posts for you, I don’t know what’s real and what is in fact generated fanfic. You definitely don’t sound like you, so…​ is the actual content real? I mean, maybe? But also maybe not. Regardless, I stop reading — and so do lots of others.

To be clear, I think LLMs are incredibly useful: they are helpful for brainstorming, invaluable for comprehending text, and they make for astonishingly good editors. (And, unlike most good editors, you can freely ignore their well-meaning suggestions without fear of igniting a civil war over the Oxford comma or whatever.) But LLMs are also lousy writers and (most importantly!) they are not you. At best, they will wrap your otherwise real content in constructs that cause people to skim or otherwise stop reading; at worst, they will cause people who see it for what it is to question your authenticity entirely.

So please, if not for the sanity of all of us than just to give your own message the credit it deserves: have some confidence in your own voice — and write your own content.

Doomscrolling Ourselves to Death

Hacker News
www.edwest.co.uk
2026-09-06 07:53:40
Comments...
Original Article

I’m not the only parent who feels increasingly anxious about their children’s media diet. For teenage boys in particular, entertainment often comes in the form of YouTube, which for any adult briefly glancing at the screen seems to feature an array of thumbnailed gawping imbeciles, besides an invitation to watch the latest work of creativity: ‘I took a bath full of Diet Coke and Mentos’ or ‘I cemented a microwave to my head’. In comparison to this, the mindless television I watched in the 1990s seems as narratively complex as Dostoevsky.

Most of all, it’s notable that many children aren’t reading anymore, and it ’s steadily become a topic of interest among fellow parents, then worry, and now panic. It’s impossible to ignore the fact that something quite bad is happening.

Every catastrophe needs its Jeremiah, and for this we have Times columnist James Marriott, whose increasing anxiety about the decline of literacy was articulated in a hugely popular substack post last year. Having written extensively about how no one reads books anymore, he decided to turn the subject into a book, The New Dark Ages , published this week.

I wouldn’t call the book ‘urgent’ because I usually take that to mean ‘worthy scolding by an ideological ally’; it’s entertaining, informative, often amusing and makes a coherent case in 50,000 or so words, which many of us can still manage. But the implications he lays out are disturbing, and influence everything around us.

James and I have become friends over the past couple of years, drawn together by a mutual sense of despair, and he’s probably the only person in journalism even more pessimistic than I am. If you believe the media to be a circle-jerk of people helping their mates (which it is, obviously), bear that in mind, but reading his prose one gets a sense of how likeable as well as articulate he is, even if born in the wrong decade. His upbringing will seem as alien to those raised in the coming years as the world of Boethius would have been to Alfred the Great.

‘Literature was our religion: the way we explained and interpreted our lives. As a child I quickly learned that every possible event from a major bereavement to a minor playground contretemps could be cross-referenced against a novel by Thomas Hardy or a poem by Philip Larkin. There was a quote for everything. I was dragged off to Shakespeare plays at what now strikes me as a rather cruelly young age (“if you’re not enjoying it”, I remember my dad telling me, “you can just read your book instead”). We worshipped pretty much all the canonical English poets but in our pantheon of literary saints, Shakespeare was Jesus or, more probably, God.

‘We celebrated his birthday every year with a homemade cake and in the holidays we were taken on solemn pilgrimages to Stratford-upon-Avon. I recall that I was once presented with a Shakespeare-themed stuffed bear (it was wearing a t-shirt with a quotation from Hamlet ).’

Such a veneration for the written word naturally made him sensitive to disturbing stories about declining literacy that began circulating around the turn of the decade. ‘The first rumours that something important was happening reached me via friends working in universities,’ he writes: ‘I came close to going into academia myself (though I don’t think the world is a poorer place for the loss of my proposed PhD on landscape imagery in post-war British poetry).’

Anecdotes were soon backed up by data, showing significant declines in reading comprehension among students, as well as an even sharper drop in the number of adults and children reading for pleasure. The chief suspect seemed obvious: it’s the phones. (Everything’s ‘the phones’). Yet the more Marriott looked into it, the more he came to believe that the real transformation came with television, as Neil Postman had warned in his 1985 polemic Amusing Ourselves to Death .

‘Postman believed that the rise of TV — with its bias towards triviality and charismatic personalities — was beginning to undermine the kinds of reasoned and productive public discourse on which a civilised democracy depends. He was dismissed by some of his critics as a crank and a Luddite. The passing of time has revealed that he was a prophet.’

Having collapsed at the end of antiquity, literacy rates began to show an uptick from the 12 th century, initially in the urban centres of northern Italy but then rapidly rising with the invention of printing; even then, however, the nascent books trade overestimated how keen the public were, and he recalls the case of ‘two hapless Roman publishers who wrote to Pope Sixtus IV at the end of the fifteenth century asking for help because they had printed 20,000 books in a fit of over-enthusiasm and were unable to sell any of them.’

Then came Protestantism, with its emphasis on reading the Bible - in Sweden the Lutheran Church even banned the illiterate from marrying - and the Enlightenment, of which Marriott is a great fan. This produced both a republic of letters among our continent’s intellectual elite, but a much broader spread of empathy, enabling many people to see beyond the categories of social class, sex or race, into the soul and mind within.

‘In a book you can find yourself paying close attention to someone you might have very easily dismissed had you encountered them in real life. The wealthy male readers of Pamela spent hours with the innermost thoughts of a serving girl, a kind of person many of them would not have given so much as a second thought to had they encountered her in the street.’

Likewise, with readers of Olaudah Equiano’s memoir The Interesting Narrative, now able to see life through the eyes of a slave in a way that was transformative: ‘what it was like to be stolen from one’s home by slave traders, packed onto a slave ship and sold like an object.’ In as much as reactionaries felt a ‘moral panic’ about the dangerous effects of reading, Marriott thinks they were right: literacy would indeed undermine a hierarchical society and bring about a more egalitarian, democratic spirit.

Marriott identifies the peak of literacy sometime in the early to mid-20 th century, and ‘by 1944, a survey found that “nearly two-thirds of skilled workers and almost half of unskilled workers grew up in homes with substantial libraries”.’ Yet already the great change was coming. In 1948, only 1 per cent of American homes owned a television; by 1954, over half did. Britain was a bit behind, but the idiot box began to take off after Elizabeth II’s Coronation in 1953, fondly remembered as a great communal event because so few people owned a set. Soon, however, television would become the least communal of activities

‘All of life seemed to be reorganising itself around television’s centrifugal attraction. In living rooms across the world, armchairs and sofas were shifted around so that they no longer faced each other (as they had in most houses for most of history) and were directed instead towards the corner that contained the screen.’

Social life turned inward, and television had a zombifying effect on many; Marriott notes that ‘in 1956 when a technical error delayed the broadcast of ITV’s Sunday Night at the London Palladium , a third of the audience had simply continued staring blankly at their empty sets until programming resumed.’

Television came to ‘devour’ our leisure time, and by 1995, as Robert Putnam wrote in Bowling Alone, 40 per cent of it was spent staring at the screen. While Putnam was writing about the United States, its effects were seen everywhere; in the Netherlands, which possesses some of the best data, the introduction of television saw a decline in the average time spent reading, from five to three and a half hours a week between the 1950s and 70s.

The number of channels exploded with the arrival of cable television, and the number of brain cells fell. Western countries began to see the ‘reverse Flynn Effect’, across-the-board IQ declines, and a natural experiment helps us to identify the culprit:

‘Because Norway is a long, narrow inaccessible country of mountains, fjords and forests, the roll-out of cable TV was staggered by region. Not everywhere got cable at once… This, plus Norway’s policy of military conscription for men, presented an interesting opportunity for social scientists. By measuring the compulsory intelligence tests taken by all military conscripts the economist Øystein Hernæs was able to map the effect on test scores of cohorts of soldiers as cable TV arrived in their areas. What he found was that as cable marched across Norway intelligence fell in its wake: “for each year that their hometown had full coverage, IQ dropped by 0.08 points”. “Higher exposure to commercial television”, Hernæs’s study bluntly concludes, “reduces cognitive ability”.’

And then things only got worse, in 2007, with the arrival of the smartphone. As Marriott points out, television has its ill effects, but it also produces something recognisable as art, and requires some degree of concentration. And whereas most people like television, they often hate their phones, seeing the terrifying power they hold.

If your household is anything like mine, it will feature parents repeatedly nagging their children to ‘get off your phone’, only for them to reply: ‘but you’re on it, too!’ ( It’s for work , I lamely reply .) Indeed, many adults struggle to limit their use of these maddeningly addictive devices, which the average person checks 144 times a day; Marriott’s own battles are comical.

‘In my early adulthood I must have spent hundreds of futile and faintly tragicomic evenings attempting to improve my reading by hiding my smartphone from myself. I have shoved my smartphone behind wardrobes and under beds. I have shut it up in cupboards. I have shoved in into the stale and musty depths of laundry baskets. One particularly desperate (though I also thought, rather ingenious) strategy I developed was to kick my phone haphazardly across the room so it would lodge under some unknown piece of furniture thus making it impossible to retrieve without laboriously hunting for it. I went through a phase of locking my phone up like an evil genie in a “smartphone prison”, a sort of miniature safe with a timer.’ Nothing could stop him succumbing to the Ring’s great power, until he got rid of the device and bought a ‘dumbphone’ instead.

The impact on writing was not initially apparent. As late as 2017, a short story in the New Yorker was read by a million people and became the subject of widespread discourse. Just nine years later the very idea seems absurd, and far more attention is given to the likes of ‘James Donaldson, the world’s most popular YouTuber and in the apt description of the writer Mark O’Connell, “the Mozart of the attention economy’’.’ His videos ‘take the form of stunts or accelerated miniature gameshows’ with titles such as ‘I Spent 50 Hours in Solitary Confinement’ or ‘Would You Sit in Snakes for $10,000?’

For those who can’t manage 30 minutes, there is always TikTok, ‘which has justly been described as the “crack” of the smartphone. It has been estimated that the average TikTok user may scroll through as many as 260 videos in the space of about half an hour.’

As our attention spans have declined, ‘statistical analysis shows that even pop songs are becoming simpler and more repetitive’, while television dialogue has become less complex, all subtext or insinuation gone as characters spell out the plot to attention-depleted audiences.

Marriott admits that ‘making this kind of argument is apt to make you sound like a reactionary bore — the kind of armchair-bound windbag who would have droned on about cultural decline all the way through the Beatles, the Rolling Stones and Joni Mitchell. I acknowledge that some readers have already made up their minds that this is precisely who I am and what I am doing. But it’s worth pointing out that the idea that culture is becoming more simple or stagnant and repetitive is not one of history’s recurring moral panics.

‘As the journalist Adam Mastroianni has neatly pointed out, for most of the twentieth century people tended to worry about the exact opposite problem: that there was too much novelty and too much pretentious difficulty. Traditional ways of doing things seemed to be continually, terrifyingly being exploded or reinvented or displaced. Art, literature, philosophy and fashion simply could not stay still.’

Today, even scientific studies are becoming ‘less disruptive’, according to one prestigious journal, ‘increasingly less likely to break with the past in ways that push science and technology in new directions.’ The culture is becoming more conservative in a way that oral societies tend to be – repetitive, static, lacking innovation.

As literacy has declined, politics has become stupider. Reading Hansard from the 1970s, and comparing it to the level of Parliamentary debate today, is a painful exercise. Classical allusions are gone; metaphor is treated literally, often provoking dim-witted outrage; speaking times continue to shrink as MPs compete in the attention economy.

Marriott contrasts Harry Truman, who ruled America during the peak of literacy, with the current incumbent. Truman ‘was the son of a livestock dealer in Independence, Missouri. And yet as an earnest and bespectacled schoolboy he spent great tracts of his time in the local library. Watched over by a plaster bust of Benjamin Franklin, Truman made his way past all the familiar landmarks of a twentieth century autodidact self-education: The Bible (which he claimed to have read through twice by the time he was twelve), Plutarch’s Lives , the complete works of Shakespeare, Plato’s Republic , Gibbon’s Decline and Fall of the Roman Empire . Eventually, he read every single one of two thousand volumes stored in Independence’s library — including all the encyclopaedias. It’s an inspiring story but not at all a unique one.’

Donald Trump, meanwhile, is so averse to reading that aides recall that he cannot even make it through one-page memos; his speeches are notably repetitive and characterised by taunting and braggadocio , a reversion to the norms of the oral age. He is an almost Homeric figure, perhaps the first post-literate western leader.

In the new media ecosystem Marriott sees a reversal of the Enlightenment, epitomised by the likes of Candace Owens, who has ‘more followers on YouTube alone than the entire subscriber base of the New York Times ’, a ‘one-woman nonsense machine’ who declares that ‘Stalin was Jewish, science is a “pagan faith” and the French Revolution was started by Jews to “bring down Christian empires”.’ He acknowledges, however, that at least she is no longer a flat earther although, as she has stated, ‘I’m not a round earther either’.

‘Well, it’s a start.’

For Marriott, ‘there is nothing inevitable or eternal about modern civilisation in all its desirable complexity and comfort’, nor is it written that it may survive ‘a world filled with demons, anti-vaccine conspiracies and “ways of knowing”’. Democracy followed the rise of literacy, and it may struggle to function in a world where the analytical way of thinking that comes with reading is alien to many voters.

While much of the most unenlightened rhetoric comes from the populist Right, the more mainstream cultural Left is terrified of appearing ‘elitist’, a charge often made to Marriott in interviews. To which, his only answer is: yes, I suppose.

‘Many of us are suspicious of hierarchies of cultural value,’ he writes: ‘To suggest that it is “better” to spend your day reading War and Peace than watching TikTok carries the dangerous ring of elitism. I was once reprimanded by a writer in a British magazine for my “belief that some writing is simply ‘better’”; a view, I was told, that is “inherently damaging, and marginalising”… A recent report issued by Britain’s most important cultural funding body, the Arts Council, warned against the use of “terms like excellence” which may promote “unhelpful hierarchies about what kinds of work are valued”.’

It is notable that the era of peak democracy in the Anglophone world, the mid-20 th century when the working class had the most political power, was also the peak of literacy. As reading has declined, so the social gap has widened: iPad children tend to come from the poorest backgrounds, while many tech titans, Steve Jobs in particular, limit the amount of technology their children access, and screens at the dinner table have become very non-U. Hierarchies grow starker when elites fail to promote high culture.

Yet the most startling thing about this book is how far even the nominally well-educated have fallen, so that ‘by the end of the twentieth century a college graduate born after 1969’ read less than someone born before 1950 with a basic level of education. Indeed, ‘nowadays many rich and highly educated people are much less well read than many members of the least privileged classes had been in the middle of the twentieth century.’

To me, that is the most salient and worrying trend: that people in positions of power and influence are now notably ignorant about history and the classics, comfortable only with referencing pop culture. I suspect that the crisis of faith in our institutions is driven not just by a sense that they are corrupt or biased, but that the people running them are not very smart. The cultural elite has abandoned cultural elitism.

Discussion about this post

Ready for more?

New all in one 6502 computer (Neo6502kbd)

Hacker News
olimex.wordpress.com
2026-09-06 07:51:28
Comments...
Original Article


Do you remember Karateka, Lode Runner, and Cross Fire on the Apple ][?

Our Neo6502kbd brings it all back — an all-in-one retro computer with a real 6502 processor that can emulate the Apple ][ and Oric Atmos, letting you play all those classic games on your modern DVI/HDMI screen or TV.

But it’s not just for gaming — you can develop with a full set of 6502 tools: CP/M, ProDOS, BASIC, Pascal, C, Assembler, and Forth.

Neo6502kbd packs everything into a single keyboard: a real 6502 CPU, 4 USB ports, HDMI output, UEXT connector for WiFi expansion, and USB-C for power and programming. Just plug in and play!

Don't Use a gmail.com Address

Hacker News
crookedtimber.org
2026-09-06 07:50:24
Comments...
Original Article

We are being flooded with spam coming from fake gmail.com addresses at the moment. So, for the moment, I’ve set our spam filter to delete everything from gmail.com

If you want to post a real comment, just give a spurious address like anon.com and use your existing name/nym. Although we ask for an email address, we almost never check that it’s real. If you have trouble with that, email me at john.quiggin@icloud.com

Hopefully, we will find a way around this soon.

Also, Google is Evil.

Building a Brainfuck DSL in Forth using code generation

Lobsters
venko.blog
2026-09-06 07:39:20
Comments...
Original Article

This article describes supporting Brainfuck as an embedded language within Forth using meta-programming. I had a lot of fun figuring this out. Like most Forth code, the resulting program is quite compact. My explanation of it is not.

I prefer to assume minimal prior knowledge from the reader and so have erred on the side of over-explanation, but for the sake of brevity I do assume that you’re at least somewhat comfortable with the common programming language concepts and the act of writing programs. If you’re already familiar with Forth and Brainfuck, feel free to skip the Introduction and head straight to The Cool Part . If you’re a Forth enthusiast and have a solid grasp on the internals of the language, you might consider also skipping the Concepts section. If you’re only interested in the final result, the full program code is included in its own section at the end of this article.

Background

Forth

Forth is a family of stack-oriented , concatenative programming languages invented and iterated on by Charles “Chuck” Moore in the 1960s.

The two dominant philosophical goals of Forth are simplicity through “highly factored code” and “direct communication with the machine”. References to these properties are found throughout Moore’s writing and lectures about Forth and in Forth literature by other authors like Rather and Brodie.

Forths are often bootstrapped directly from assembly without a host operating system, acting as a sort of supercharged macro language over the native machine code. Though language standards for Forth exist, most Forth implementations are highly idiosyncratic and contingent on their target hardware. Language amenities like conditional branching, looping, and so on are only defined if necessary for solving the problem at hand. The names of operations are chosen based on whatever mnemonics the user finds most convenient.

The lack of extraneous features and tight coupling with the machine makes Forths well suited for austere, resource constrained environments with strict performance requirements. For example, Forths have been deployed to good effect in space probes and satellites.

At the same time, their bootstrapped nature leaves all machine and language internals open to the programmer for modification, making Forths remarkably amenable to meta-programming. As a currently-relapsing Lisp programmer, I find Forth’s meta-programming ability highly compelling.

Brainfuck

Brainfuck is perhaps the most well known “esoteric” programming language. It was designed in the early 1990s by Urban Müller as an experiment in making as small of a compiler as possible. It is Turing-complete (that is, exactly as powerful as “real” languages like C) despite only supporting eight instructions. This mix of power and simplicity has made writing a Branfuck interpreter a common project for programming hobbyists.

Brainfuck is usually implemented as a very simple virtual machine. The machine comprises four elements:

  1. Main memory - The data manipulated by the program. By convention, Brainfuck machines typically have 30,000 bytes of memory. Memory is accessed one byte at a time. Note that this memory does not include the program code. Code is stored in a different section (see below).
  2. Data pointer - An index into main memory. Brainfuck machines are able to modify their memory one byte at a time by first shifting the data pointer to a location and then executing certain instructions.
  3. Code pointer - Brainfuck machines execute code following the conventional Von Neumann cycle. The code pointer indexes the operation in the program code that will be executed in the upcoming iteration of the cycle.
  4. Code - The program being executed by the Brainfuck machine. Brainfuck supports 8 operations:
    1. > and < for incrementing and decrementing the data pointer, respectively.
    2. + and - for incrementing and decrementing the byte in memory addressed by the data pointer, respectively.
    3. . for printing the byte of memory addressed by the data pointer. Note that this operation assumes the byte in memory is valid ASCII.
    4. , for consuming a single character of input from the keyboard and writing it to the byte addressed by the data pointer. This operation also assumes an ASCII representation for input.
    5. [ and ] for performing conditional branching and looping. The [ and ] operations must appear in matched pairs and act as delimiters for blocks of code. The [ word allows the program to enter its block if the bye of memory addressed by the data pointer is not equal to zero. Otherwise, the code pointer is shifted forward to the matching ] . Similarly, when ] is encountered, the code pointer is shifted backward to the corresponding [ if the byte of memory addressed by the data pointer is equal to zero.

The Cool Part

By now you might have an idea of how you’d write your own Brainfuck interpreter. A couple variables for the pointers, a big byte array for the memory, some kind of string buffer to hold the code, and some functions for reading and executing instructions. Maybe an extra helper function so you can load code from files. However, this article isn’t about writing a Brainfuck interpreter. I want my Brainfuck code to itself be valid Forth code, able to be embedded straight into a Forth program. This article is about writing a Brainfuck DSL .

This brings us to The Cool Part: Not only is embedding a Brainfuck in Forth totally possible with Forth’s meta-programming facilities, it’s actually easier than doing it the regular way. In fact, our DSL won’t even model the code or code pointer of the Brainfuck machine. It doesn’t need to, the code is already in the interpreter.

Before writing any code, we need to dig into how Forth works under the hood a bit.

Concepts

Forth is highly factored code. I don’t know anything else to say except that Forth is definitions. If you have a lot of small definitions you are writing Forth. In order to write a lot of small definitions you have to have a stack.

-Chuck Moore, “1x Forth”, 1999

At the heart of every Forth is a pair of stacks – one for data, the other for control flow – and a dictionary for definitions. Values are pushed onto the data stack and consumed by words in the dictionary. Words are Forth’s name for subroutines. Traditionally, the dictionary is represented using a linked list. New word definitions are appended onto the dictionary as the program runs. When the programmer types in a word and hits Enter, the dictionary is searched from newest definition to oldest until an entry with the word’s name is found. The code of that word is then executed.

I’d like to dwell on this a moment: words are subroutines but shouldn’t be confused with the functions or procedures of most other languages. Rather than accepting a set of parameters explicitly passed by the caller, Forth words operate on whatever values happen to be on the stack at the time. They are free to pop and push however many values from and to the stack as they please. Any number of words can be written and executed in sequence successfully so long as each word’s assumptions about the data on the stack are true when that word executes. Likewise, words can be split internally into multiple separate words (with some exceptions). This is what makes Forths concatenative ; the ability to splice and chunk sequences of words is what allows the programmer to build their “lot of small definitions”.

Forth basics speedrun any% [PB] [2025]

I didn’t initially write this with a plan to include a section on the fundamentals of Forth. The meat of the article is about a pretty advanced feature of the language, after all, but I would feel bad if I scared readers off just for refusing to write a few sentences. If you’re totally unfamiliar with Forth, please check out a proper introduction to the language like GForth’s tutorial or A Beginner’s Guide to Forth .

Forth programs are their own interactive runtime environment, that is, the Forth interpreter is just another Forth program. Forth code has almost no syntax. Any chunk of text delimited by whitespace is a valid lexical token. Numeric literals are recognized by the environment as values, and any other token is treated as the name of a piece of code to be executed. Like most stack-oriented languages, Forth code is written in Reverse Polish Notation .

So when you load up a Forth interpreter and type something like…

1 2 + .

… you are instructing Forth to do the following:

  1. Push the value 1 onto the stack.
  2. Push the value 2 onto the stack.
  3. Execute the + word, which pops two values off of the stack sums them, and then pushes the result back onto the stack.
  4. Execute the . word, which pops a value off of the stack and writes it to the screen.

Values on the stack can be manipulated. A value on the stack can be duplicated with dup or discarded with drop . A pair of values can be transposed with swap . Trios of values can be spun around with rot and -rot . I always forget which direction is which.

If you find yourself writing the same chunk of code a lot, you can factor that code into a new word using the : and ; words.

: print-three 1 2 + . ;

The : word tells Forth that the next token is the name of a new word ( print-three in this example). All code between the name and the ; is the definition of the new word. Words can be redefined at any time. Code depending on a prior definition of a word is not affected, only later definitions can see the redefined word.

Forth supports conditional branching inside word definitions.

: ?even 2 mod 0= ;
    
: print-parity
     ?even if ." Number is even!" else ." Number is odd!" then ;

It also supports iteration within words. Common looping constructs include begin ... while/until ... repeat for indefinite conditional looping and do ... loop for counted loops.

Here’s some code that writes the Collatz sequence for a number:

: ?even 2 mod 0= ;
: halve 2 / ;
: triple-plus-one 3 * 1 + ;
: collatz
    begin 
        dup . ( print the number)
        dup 1 > 
    while
        dup ?even if halve
        else triple-plus-one
        then
    repeat
    drop ( the number)
;

Interpretation versus Compilation

Forth programs are executed as they run. Each time the user types code into the interpreter and presses Enter, the line of code is read and executed bit-by-bit. Perhaps counterintuitively, this includes code whose result is to define new words. If it’s not clear why this might be counter-intuitive, read on.

Imagine we’re writing a program that’s very preoccupied with taking quantities on the stack and doubling them. Doubling a value is as simple as duplicating it (pushing a copy of it onto the stack) so it can be added to itself. After a while, we get tired of writing dup + and decide that we want to factor that code into a word called dub .

We can define our new word like so:

: dub dup + ;

Each word in Forth has a textual definition denoting the word’s interpretation semantics , that is, the machine code or other Forth words which will be executed when the word is invoked. Therefore, whenever we encounter a dub in our code, we know that Forth will first perform a dup and then follow it with a + .

Now let’s pretend we’re Forth and we’re reading the definition of dub . We read input one white-space delimited word at a time. The first word we encounter is : , which is how Forth spells “define”. The interpretation semantics of : are to prepare the dictionary for a new definition corresponding to the next word in the input stream, dub . We initialize a new dictionary entry named dub . Next we encounter dup , so we should immediately duplicate whatever’s currently on the stack, right? After all, that’s what dup ’s interpretation semantics are.

Obviously not. Words only having interpretation semantics would make defining new words impossible. Like other languages, Forth does something different when it encounters a word that’s being compiled into the definition of another. Unlike other languages, compilation is transient; Forth can toggle between its interpretation mode and compilation mode at any time. When in compilation mode, each word encountered executes according to its compilation semantics . By default the compilation semantics of a word are to find the word’s address in the dictionary and insert code for invoking it into the current definition.

So in our definition of dub we know that dup and + are simply being compiled and won’t run until dub is invoked. But the next word is ; , which is supposed to indicate the end of a word definition. When Forth encounters ; it’s supposed to do all the bookkeeping required to finalize the definition of a new word, but if ; ’s compilation semantics are the same as other words, then we’d expect Forth to attempt to compile it into the current definition. How does ; avoid that and just tell Forth we’re done defining?

Clearly there’s something more interesting going on in the definition of ; . Forth is an interactive language that doesn’t believe in keeping secrets. Let’s hop into an interpreter and ask it what ; means.

see ; \ <- Only type this line
: ;
  ;-hook  [COMPILE] EXIT flush-code ;-hook2 ?colon-sys reveal  [COMPILE] [ ; immediate compile-only ok

That’s weird, ; appears in its own definition. My chosen Forth dialect is GForth, the GNU Forth. GForth is “bootstrapped”, meaning it defines itself in itself. Let’s put that detail aside.

I’m going to gloss over some details here to avoid ending up in the weeds. The definition of ; does a few interesting things. First, it compiles an EXIT (Forth’s name for returning by popping the return stack) into whatever the current definition is. Then it does some bookkeeping work in the dictionary. Finally it compiles the word [ into the current definition. The [ word toggles Forth from compilation mode back into interpretation mode. (If you look at the definition for : , you’ll find a corresponding ] that toggles compilation mode).

Note that I mentioned the “current definition” a couple times just now. You may be thinking, “Isn’t the ‘current definition’ in question the one for ; ?”. It is not. Take a look at the pair of words right after the end of ; ’s definition: immediate compile-only . Working backwards, compile-only is an indication that the most recently defined word, ; in this case, should only ever be used while in compilation mode. In other words, ; only has compilation semantics. There’s no sensible usage of ; other than terminating a new word’s definition, which necessitates being in compilation mode.

But what about immediate ?

The most important part of Forth

The immediate word indicates that the most recently defined word is immediate . Immediate words execute the moment they are encountered in the input stream, even while the system is in compilation mode.

Therefore, the “current definition” I mentioned before isn’t the definition of ; , but is instead the definition of whatever new word is being compiled when Forth executes ; . It has the effect of writing instructions directly into the new word’s definition.

In his article “The Forth Programming Language - Why YOU should learn it”, Doug Hoyte (author of the delightful Let Over Lambda ) writes:

Immediacy is a very powerful feature in forth. Once you understand how immediate words are used to build up everything in the language, you will have understood the most important part of forth.

Immediacy is the most important part of Forth because it allows the programmer to write code that executes at compile-time. This seems like an advanced feature reserved for Lisp hackers (and indeed Lisp’s reader macros and Forth’s immediate words are two solutions to basically the same problem), but compile-time code is actually ubiquitous in Forth. The definition terminating word ; is immediate. Conditional branching works because if , else , and then are immediate. The same is true for repetition with begin / repeat , conditional repetition with while and until , and counted repetition with do / loop , etc. Even comments use immediate words.

Thank you, five

There’s still a bit more to cover but this is a good place to pause, take a break, and review.

Forth lets the user break code into named chunks called words by compiling their definitions into a dynamic data structure called the dictionary. To support this, the Forth runtime can be toggled between interpretation and compilation modes on demand. Words usually have two sets of semantics, one for what happens when the word is interpreted and another for when the word is compiled. Some special words only have the latter. Words that are “immediate” execute the moment they are read, regardless of whether the Forth is currently interpreting or compiling.

Alright, back to it.

Postponed words

Assuming you’ve understood everything so far, there’s just one final piece to make our Brainfuck DSL possible: postponed words . The postpone word, which is compile-only , parses the next word in the input stream and appends the compilation semantics of that word into the current definition, even if that word is marked immediate. In other words, postpone compiles the compilation semantics of one word into the interpretation semantics of another.

Immediate words and postponed words work in opposite directions. Immediate words allow the programmer to execute code at compile time. Postponed words let the programmer defer what would normally be done at compile-time until run-time.

I lack Doug Hoyte’s bona fides but if immediate words are the most important part of Forth, then I think postponed words are in the running for second. Let’s start building the program to see why.

Building the program

With that background out of the way we’re ready to start writing some code. One of the best parts of Forth is that it’s interactive. I’ve chosen GForth as my dialect for this article. If you have it on your machine, feel free to paste code from this section straight into the interpreter. As you read, recall that the \ word begins an until-end-of-line comment and ( comments everything on a line until the comment block is terminated with ) .

Setting up

To start things off, let’s get our Brainfuck machine defined.

variable dp
create memory 30000 chars allot

This just gives us an integer for our data pointer and a block of 30,000 bytes of memory. Note the lack of a code pointer variable or an allocation for the program code.

Next, let’s write some helper words.

: reset
   0 dp !
   memory 30000 chars erase ;

: show
   cr ." dp: " dp @ .
   cr ." memory[dp]: " m[dp]@ .
   cr ." memory: " 10 0 ?do i memory + c@ . loop ;

: inc!  1 swap +! ;
: dec! -1 swap +! ;
: m[dp]  dp @ memory + ;
: m[dp]@ dp @ memory + c@ ;
: m[dp]! dp @ memory + c! ;

These words let us reset the state of the Brainfuck machine, dump debug information about it to the screen, and do some basic manipulation of the data pointer and the byte it indexes.

Defining the DSL

Each of the words defined in this section follow an interesting pattern:

: <word> ]] <instructions...> [[ ; immediate

Recall that postpone defers the following word’s compilation semantics until interpretation. To save users from writing postpone repeatedly, GForth provides the words ]] and [[ . The first word puts Forth into a mode where every word encountered is automatically postpone ’d until the corresponding [[ is found. The following two definitions are equivalent:

: a postpone foo postpone bar postpone baz ;
: b ]] foo bar baz [[ ;

The first six Brainfuck operations are straightforward. A little bit of familiarity with Forth should be enough for you to understand them:

: >    ]] dp inc!     [[ ; immediate
: <    ]] dp dec!     [[ ; immediate
: +    ]] m[dp] inc!  [[ ; immediate
: -    ]] m[dp] dec!  [[ ; immediate
: .    ]] m[dp]@ emit [[ ; immediate
: ,    ]] key m[dp]!  [[ ; immediate

Branches and loops

The final two commands are where the real magic happens. Let’s begin with their definitions.

: [    ]] m[dp]@ 0<> if begin  [[ ; immediate
: ]    ]] m[dp]@ 0= until then [[ ; immediate

Note that the if inside of [ lacks a terminating then . The begin also lacks a terminating repeat / until . The ] word has the same issue. Without the power of postpone , these words would fail to compile. But why?

Recall that conditional branching and looping in Forth is achieved with immediate words. The reason for this is that Forth does not perform a separate lexing/parsing step when processing code inputs. Words are consumed one at a time and trigger some kind of action by the Forth runtime as they’re encountered. Branches and loops are achieved in Forth by defining words like if and begin to leave marker values inside the currently compiled definition. When a corresponding terminating word like then or repeat is encountered, the runtime scans back to find the marker value and replaces it with a jump instruction.

Scanning through a definition one instruction at a time? Jumping by some number of instructions? This sounds like what [ and ] do in Brainfuck! Indeed, the reason why our Brainfuck machine doesn’t need an allocation for the program code or a code pointer variable to index into it is because Forth already does the equivalent thing when compiling branches and loops. All we have to do is ensure the branching and looping semantics match [ and ] .

Trying it out

I’ve intentionally avoided explaining exactly what each of the DSL words above really do because I think a visual explanation is much more illustrative (and exciting). But for that, we’ll need Brainfuck code to test out. Thankfully we don’t need a large program to showcase each of Brainfuck’s operations. This one is adapted from an example in Brainfuck’s Wikipedia article .

: seven
   + + > + + + + + 
   [ < + > - ] 
   + + + + + + + + [ < + + + + + + > - ] < .
;

The article describes the program in detail, but to summarize: the program places the value 2 in one memory cell, the value 5 in another, sums the two cells together to produce 7, and then encodes 7 as an ASCII value and prints it to the screen.

Go on, type seven into your interpreter and see it work:

seven 7 ok

Now for the big reveal. Let’s see seven ’s real definition.

see seven
: seven
  m[dp] inc! m[dp] inc! dp inc! m[dp] inc! m[dp] inc! m[dp] inc! m[dp] inc! m[dp] inc! m[dp]@ 0<>
  IF     BEGIN  dp dec! m[dp] inc! dp inc! m[dp] dec! m[dp]@ 0=
         UNTIL
  THEN
  m[dp] inc! m[dp] inc! m[dp] inc! m[dp] inc! m[dp] inc! m[dp] inc! m[dp] inc! m[dp] inc! m[dp]@ 0<>
  IF     BEGIN  dp dec! m[dp] inc! m[dp] inc! m[dp] inc! m[dp] inc! m[dp] inc! m[dp] inc! dp inc! m[dp] dec! m[dp]@ 0=
         UNTIL
  THEN dp
  dec! m[dp]@ emit ; ok

Here we see the magic of postponed words. Defining each Brainfuck operation as an immediate word full of postponed words means that the actions applied to the Brainfuck machine are compiled straight into a word’s definition in-line. Immediate postponed words act as code-generating macros that emit plain old Forth code.

This means we can write parts of a program in Brainfuck and parts of it in Forth, even within the same word definition! Is seven feeling a bit too unreadable? Let’s just make some helper words and redefine it:

: add-left-to-right ]]
  [ < + > - ]
[[ ; immediate

: digit->ascii ]]
  + + + + + + + + [ < + + + + + + > - ] <
[[ ; immediate

: seven
   + + > + + + + +
   add-left-to-right
   digit->ascii
   .
;

Conclusion

What I’ve tried to showcase here is just a taste of the kinds of meta-programming Forth lets you do. Consider, for instance, that so-called “parsing words” can take direct control of the Forth input stream, or that code-emitting words can operate conditionally based on a larger context. My DSL is simple and useless in roughly equal measure. I hope that you’re inspired by seeing how it was able to be written using such an austere language with so few moving parts.

What I can promise is the perspective from a simple, passionate programmer of a couple decades, and 3 years of those belong to Forth. Never has my heart been completely enveloped by an idea. The allure and compulsion has practically caused me to fall in love with a programming language.

Lee, “An Attempt at a Compelling Articulation of Forth’s Practical Strengths and Eternal Usefulness”, 2025

I have a lot of thoughts about programming languages in general, and Forth in particular. I delight in learning new langauges and paradigms. Feeling my brain bend and stretch into new shapes as it gradually learns to think about problems in new ways is the thrill that keeps me hooked on writing programs. Few languages have challenged and rewarded me to the extent that Forth has. Indeed, one of the most challenging aspects of Forth is articulating exactly what makes the language so special. I hope to expand on those themes in future articles.

The full program

variable dp ( data pointer) 
create memory 30000 chars allot

\ Helper words for manipulating memory at the data pointer
: inc!  1 swap +! ;
: dec! -1 swap +! ;
: m[dp]  dp @ memory + ;
: m[dp]@ dp @ memory + c@ ;
: m[dp]! dp @ memory + c! ;

\ Brainfuck commands
: >    ]] dp inc!              [[ ; immediate
: <    ]] dp dec!              [[ ; immediate
: +    ]] m[dp] inc!           [[ ; immediate
: -    ]] m[dp] dec!           [[ ; immediate
: .    ]] m[dp]@ emit          [[ ; immediate
: ,    ]] key m[dp]!           [[ ; immediate
: [    ]] m[dp]@ 0<> if begin  [[ ; immediate
: ]    ]] m[dp]@ 0= until then [[ ; immediate

\ ========== Demos ==========

\ Plain Brainfuck definition for adding two numbers
: seven
   + + > + + + + + 
   [ < + > - ] 
   + + + + + + + + [ < + + + + + + > - ] < .
;

\ Helper words over Brainfuck code
: add-left-to-right ]]
   [ < + > - ]
[[ ; immediate

: digit->ascii ]]
   + + + + + + + +
   [ < + + + + + + > - ] <
[[ ; immediate

: seven
   + + > + + + + +
   add-left-to-right
   digit->ascii
   .
;

\ Another Brainfuck example, just for you :)
: hello-world
   reset
    + + + + + + + + [ > + + + + [ > + + > + + + > + + + > + < < < < - ] > + > +
    > - > > + [ < ] < - ] > > . > - - - . + + + + + + + . . + + + . > > . < - .
    < . + + + . - - - - - - . - - - - - - - - . > > + . > + + .
;

'Old Person Smell'

Hacker News
www.theguardian.com
2026-09-06 07:37:27
Comments...
Original Article

Last week, my mom complained that she’s starting to smell “like an old person”.

She had grabbed her favorite sweater and draped it over herself at the movie theater. “It smelled like my grandmother,” she said, almost whispering. “I thought someone must have borrowed the jacket. But it was just me.”

My mom, who’s in her 60s , has a ton of energy. Her hair is thick and still naturally chestnut. She’s healthy and strong; she likes to hike and go to rock concerts.

“I don’t feel like an old person. Why do I smell like one?” she whined.

I told her, truthfully, that I didn’t notice any new aroma. I thought she smelled the same as always: a mix of lotion and gardenia. But I immediately knew what she was talking about.

Experts say that we do smell different as we age. “It’s one of the most universal, least talked-about parts of being human,” says Dr Roya Javid, a dermatologist in Monterey, California. “It’s just biology, unfolding on schedule, the same way we grow taller, then our hair goes gray . Every single person on the planet goes through these same transitions.”

What causes us to smell different as we age?

The musty odor that people sometimes call “old person smell” comes from a chemical compound called 2-nonenal. 2-nonenal is produced when certain skin oils oxidize and break down, says Javid.

It’s a normal – and unavoidable – change.

“Our skin is coated in a thin layer of natural oils called skin lipids. They keep skin soft, waterproof and protected,” she says. Those lipids are made up of fatty acids, and as we age, the fatty acids change in composition. At the same time, the skin’s ability to fight off every day wear-and-tear slows down. The oils are more exposed, and when they break down through oxidation, they release 2-nonenal.

“It’s just a natural byproduct of skin ageing – not dirt, not poor hygiene,” Javid says. “It’s just oil quietly changing over time, the same way metal changes when it’s exposed to air.”

Levels of 2-nonenal vary by person. “We don’t yet know why some develop a stronger scent than others,” says Javid. “Though an individual’s skin-oil composition, genetics, the skin’s microbiome, hygiene practices, clothing, health and environment may all play a role in both how much 2-nonenal is produced and how much stays on the skin throughout the day.”

When does this change occur?

Experts say “old person smell” can show up much earlier than you might expect.

A Japanese study from 2001 tested the skin of subjects between the ages of 26 and 75, and detected 2-nonenal starting at age 40. But Dr Arshad Rather, consultant geriatrician and general physician at Medical Express Clinic in London, says this doesn’t mean people wake up with a noticeable new odor.

“Research suggests 2-nonenal increases gradually the older you get, particularly past middle age. It’s not a switch flipping at 40 – more a dial slowly turning up,” says Rather.

Experts note that developing an age-specific odor isn’t unique to older adults. In fact, every age group has associated smells, for better or worse.

“Every stage of life has its own signature scent,” says Javid. “Babies smell sweet and milky because their scent glands haven’t switched on yet. Puberty flips that switch, and suddenly hormones drive a stronger, muskier smell for teens. Adulthood has a more consistent scent tied to sweat and daily life. And [these] changes in the skin’s natural oils give people a different scent later on.”

Is ‘old person smell’ inherently bad?

Any negative feelings people have toward “old person smell” are really a perception problem, not a reality problem, Javid says.

skip past newsletter promotion

A blind test found that the 2-nonenal scent may not be disagreeable. In one 2012 study , researchers asked 41 young adults to smell T-shirts worn by individuals from three age groups: young (20–30 years old), middle-age (45–55) and old age (75–95). The researchers found that the odor from older adults was “rated as less intense and less unpleasant” than other groups.

Dr Johan N Lundström , a professor of psychology at Karolinska Institutet in Sweden and one of the authors of the study, says his team was surprised by the results. They hypothesized that smell would follow a linear trend, with the youngest group being rated highest and the oldest group rated the lowest, but that wasn’t the case.

“The elderly have a clearly separate body odor quality, but it was not rated as being all that negative,” Dr Lundström says.

Can you do anything about the odor?

Experts say there’s no way to stop 2-nonenal from developing. But there are some ways to manage the odor.

Javid says regular bathing can wash away some of the scent. However, soap and water won’t fully remove the chemical, because it doesn’t simply sit on the surface of the skin like sweat – rather, it’s being generated continuously. Deodorants might temporarily cover up a smell, but won’t remove 2-nonenal.

Meanwhile, regularly washing clothes, sheets and airing out rooms can help reduce the odor in the home. But because 2-nonenal won’t easily wash out, it can linger even after the most dedicated cleaning practices.

Some body wash products are marketed as neutralizing 2-nonenal, such as soap made with persimmon. Experts say while there are some anecdotal accounts that these help, scientific evidence is lacking.

Rather suspects there’s a connection between 2-nonenal production and a person’s lifestyle and diet, but says more studies are needed. “Since the smell comes from an oxidation reaction, reducing oxidative stress is a sensible target. Eating antioxidant-rich foods, such as colourful fruit and vegetables and sources of vitamins C and E, can reduce oxidative stress,” he says.

“Not smoking and protecting your skin from sun damage are sensible anti-oxidation habits too,” adds Rather.

In the end, experts say it’s true that people develop a different smell as they age, but it’s nothing to be embarrassed about.

If a person starts to notice they smell like 2-nonenal – which we all will – Javid urges them to not feel self-conscious or spend a fortune on special soaps.

“Focus on the things that support your skin generally: antioxidants, less sun damage, less smoking, good hydration,” she says. “And know that the science says you’re worrying about something that, objectively, isn’t as bad as the stereotype.”

Russell Coker: CoMaps

PlanetDebian
etbe.coker.com.au
2026-09-06 07:05:50
I have just tried CoMaps, a free mapping program released under the Apache license [1]. I have tried it on Android on a Pixel 6a but it also runs on Linux so I’ll try it on a PinePhone or similar at some convenient time. On Android it is in the F-Droid repository among others and for Linux there’s a...
Original Article

I have just tried CoMaps, a free mapping program released under the Apache license [1] . I have tried it on Android on a Pixel 6a but it also runs on Linux so I’ll try it on a PinePhone or similar at some convenient time. On Android it is in the F-Droid repository among others and for Linux there’s a Flatpak package.

The data it uses is from Open Street Map project [2] which has extensive and accurate coverage of every place I’ve looked at (Australia and a few other first-world countries). The first thing it does after being installed is start downloading the world data set from Open Street Map and prompt to download the data for the detected region (Melbourne in my case).

The UI is decent and allows most of the features that I am used to using in Google Maps. The quality of directions seems good, I’ve only tested it with one journey so far which was a 50 minute drive across the city and it gave a set of directions that Google Maps often gives.

It gives spoken directions which is an important feature but sometimes the way the directions are presented is confusing. When turning off a freeway it didn’t give a spoken direction to do that, it gave a direction to “turn right” which was AFTER leaving the freeway, fortunately the map was clearly displayed.

In terms of use practices of this program the main difference I recommend is checking which off ramp to use from a freeway before entering the freeway. With Google Maps you can rely on it giving clear directions in that case.

I recommend this program without reservation. It can do everything that Google Maps does apart from detecting traffic jams because there’s no way of detecting traffic without spying on users. It is designed to preserve user privacy and works well in that regard.

I asked astra to make playable 4D chess

Hacker News
4d-chess.pages.dev
2026-09-06 07:04:07
Comments...
Original Article

White to move

Tesseract projection

Four-dimensional board projected into 3D

W = 0 is the inner cube. W = 1 is the outer cube.

How to play, from 3D to 4D

Back to game ↑

The goal is still checkmate. White moves first. Select a piece, then a marked square. A solid dot is an empty destination; a ring marks a capture. In computer mode, choose White or Black using the side selector.

1. Give your 3D board one more coordinate

Think of a 3D chessboard as layers of ordinary boards. X and Y locate a square within a layer; Z chooses the layer. Here, each 3D board has two 4 × 4 layers.

Now make a second copy of that whole 3D board. W chooses which copy you are in. Every square has a corresponding square in the other copy. A move along W changes copies while keeping X, Y, and Z the same.

In the tesseract view, W = 0 is the inner cube and W = 1 is the outer cube. Both represent equally sized spaces. The inner cube looks smaller because of the projection. W is another spatial direction; it does not mean time.

The four coordinates on your screen
Axis Where it takes you
X Across a board, files a to d.
Y Up a board, ranks 1 to 4.
Z Between the left and right boards in the same row.
W Between the bottom and top boards in the same column.

a1 [0,1] means file a, rank 1, Z = 0, W = 1. That is a1 on the top-left board.

2. Compare a move in 3D and 4D

Choose a piece, then play or scrub through its move. These cleared-board examples use this game's rules. Other 3D chess variants may define their pieces differently.

In one 3D board
In the 4D tesseract

Before

Drag either drawing to rotate both. You can also focus a drawing and use the arrow keys. Home resets the views.

Same 4D move on the flat boards

These diagrams illustrate movement. In your game, pieces along a sliding path block it, and every move must keep your king safe.

3. Count the coordinates that change

Judge a move by its coordinates. A W move can look diagonal in the drawing and still count as a straight rook move. Rotating the view changes how a move looks, not whether it is legal.

Movement rules for all six pieces
Piece Movement in this game Watch for
Rook Change exactly one coordinate by any distance. Changing W and Z together is a diagonal, so a rook cannot do it.
Bishop Change exactly two coordinates by equal distances. X + W or Z + W works. X + Y + W changes three axes and does not.
Knight Change one coordinate by two and a different coordinate by one. Jump over pieces. The two-step part must use X or Y. Z and W have only two positions.
Queen Change any nonempty combination of coordinates by equal distances. One step on all four axes is legal. Two on X and one on W is not.
King Change any nonempty combination of coordinates by one step. Every destination must be safe, including threats from another cube.
Pawn Advance one empty square along Y. Capture one Y step forward plus one step along exactly one of X, Z, or W. White advances toward rank 4; Black toward rank 1. Reaching that rank automatically makes a queen.

Any bishop or queen move involving Z or W is at most one step long on this board. Those axes only run from 0 to 1. X and Y each have four positions.

4. Look for threats across W

A king can be in check from a piece in the other cube. For example, a rook at a1 [0,0] attacks a king at a1 [0,1] along W. Distance on the screen gives you no protection.

You may capture the attacker, block a longer sliding attack, or move the king, provided the resulting position is safe. A one-step W attack has no intermediate square to block. Checkmate means your king is attacked and no legal reply escapes it.

Two lines crossing in the projection do not necessarily share a square. A piece blocks a sliding move only when its full X, Y, Z, W position lies on the path. Knights jump.

Your first W move

From the starting position, choose White's rook on a1 in the bottom-left board. Select a1 in the top-left board. The rook goes from a1 [0,0] to a1 [0,1] . Only W changes.

In the tesseract, that is a move from the inner cube to its matching point on the outer cube. The rook is still moving along just one axis.

Try it on the board ↑

From 2D chess to 4D chess: X and Y select a square on a 4 by 4 board; Z selects one of two layers; W selects one of two connected stacks. The 4D board has 64 squares. Moving along W switches stacks while X, Y, and Z stay fixed. The nested cubes represent a 3D projection of 4D space.

Rules

Win by checkmating the opposing king. White moves first. Select one of your pieces, then a marked destination. In computer mode, choose White or Black using the side selector.

One space, four coordinates. Each square has an X file, a–d, and a Y rank, 1–4. The two board columns are Z = 0 and Z = 1. The two board rows are W = 1 at the top and W = 0 at the bottom. A square is written as a1 [0,0] , with Z and W inside the brackets. Moving to the same square on an adjacent board changes just one axis.

Each side starts with a king, queen, two rooks, bishop, knight, and four pawns. The following movement rules apply equally to X, Y, Z, and W.

Rook

Move any distance along exactly one axis. It can travel within a board or to the same square on another board in the same row or column.

Bishop

Move the same distance along exactly two axes. This includes ordinary diagonals and diagonals that cross between boards.

Queen

Move the same distance along any number of axes, from one to all four. Every changed coordinate must change by the same amount.

King

Move one step along any combination of axes. It may cross both Z and W in a single move, but may never enter an attacked square.

Knight

Jump two steps along one axis and one along a different axis. It jumps over pieces. On this board, the two-step part must use X or Y.

Pawn

Move one empty square along Y. White goes toward rank 4, Black toward rank 1. Capture one Y step forward plus one step along exactly one of X, Z, or W. Reaching the last rank automatically promotes to a queen.

Blocking and check. All pieces except knights are blocked by pieces along their path. You cannot land on a friendly piece or leave your king in check. Checkmate ends the game; the king is never captured.

Pawns have no opening double move. There is no castling or en passant. Stalemate, a third repetition, 100 turns without a pawn move or capture, and two bare kings are draws. Each player's turn counts as one move in the journal.

Try a fourth-axis move. At the start, select White's rook on a1 in the bottom-left board. Move to a1 in the top-left board. X, Y, and Z stay the same; only W changes.

Keyboard. Tab to a square, then press Enter or Space to select a piece or move. Arrow keys move focus within a board. Alt + arrow keys move focus between boards. Escape clears your selection.

Move history

4D PGN notation

Ninja Artisan electric pizza oven review: so good it might even convert the purists

Guardian
www.theguardian.com
2026-09-06 07:00:04
Affordable, fast-heating and with a clever air fryer function, this versatile model is a winner for outdoor cooks who are willing to give up the traditional flame • The best pizza ovens – tested• Gozney Dome Gen 2 pizza oven review An electric pizza oven with no flame and no need to rotate the pizza...
Original Article

A n electric pizza oven with no flame and no need to rotate the pizza may seem sacrilegious to purists, but the Ninja Artisan electric outdoor pizza oven solves many of the hassles of outdoor pizza making. There’s no lighting or monitoring a flame, no need to keep adjusting the heat, and no need for a turning peel, bulky gas bottle or constant use of an infrared (IR) thermometer (although you may still want one – read on for details).

Equipped with four presets – Artisan, Thin Crust, New York, Pan – plus a custom setting to enable you to bake your pizza exactly how you like it, the Ninja Artisan is as effortless as can be, even next to the simplest plug-and-play gas oven. As you might expect from Ninja, it’s a multipurpose cooker, too, transforming into an outdoor air fryer with a capacity to turn out up to 1.3kg of french fries, or bake six chicken breasts.

Being electric also means fast heat-up – useful if a rain shower is looming. In tests, it reached 370C in 14-15 minutes, and pizzas were perfectly cooked in just two minutes, despite the three-minute preset. At an RRP of £314.99 (but £229.99 at the time of writing), it costs about the same as a compact gas oven but is cheaper to run, using less than 1kWh of electricity after an hour at full temperature.

View at SharkNinja
View at Amazon

How I tested

Testing the temperature of the Ninja Artisan with an infrared thermometer
Hot take: an infrared thermometer tracks how quickly the Ninja gets up to temperature. Photograph: Rachel Ogden/The Guardian

I tested the Ninja Artisan electric outdoor pizza oven & air fryer MO201UK in the same way I test all pizza ovens I review for the Filter , although the temperature range here is different.

As well as judging the quality of pizzas cooked with different toppings – cheese and tomato at 370C; a veggie pizza with onion, pepper and tomato at 220C to slowly caramelise the toppings; and a salami pizza at 245C – I rated the oven for ease of assembly, overall usability, temperature control and how easily it could be stored.

I also recorded how long it took the oven to heat up to its maximum temperature of 370C (which is lower than the maximum on gas and wood-fired ovens).


What you need to know

An uncooked pizza ready to go into the Ninja Artisan pizza oven.
Ready to cook: the Ninja’s electric setup makes spur-of-the-moment pizza nights easier. Photograph: Rachel Ogden/The Guardian
View at SharkNinja
View at Amazon

Those with gas or wood-fired ovens are likely to need to plan pizza nights, ensuring they have enough fuel and that the weather will hold out long enough for both the oven heat-up and the cooking time.

With the Ninja Artisan electric outdoor pizza oven, you can be more spur-of-the-moment. Simply plug it into an outside socket, and heat-up is fast, with a beep to signify it’s ready to use. And because there’s no flame and the heat is evenly distributed, there’s no need to even rotate your pizza mid-cook.

You’ll need to keep a close eye on cooking time, however. I was caught out with my first pizza, so I shortened the cook time for the next. The Artisan’s door also takes some getting used to: unlike standard open-fronted pizza ovens, you’ll need to factor in time to open it to check or remove your pizza before the base or crust starts to char. Every second counts.


Ninja Artisan electric outdoor pizza oven & air fryer MO201UK specifications

Dimensions: 40.31 x 57.2 x 31.39cm (WDH) / mouth 34 x 12cm (WH)
Running cost for one hour on maximum: 25p (£0.25/kWh x 0.989kWh; 15-min heat up, 45mins at 370C)
Maximum capacity: 30 x 31cm pizza stone (working capacity 12in pizza)
Features: four functions: Pizza , Bake, Air Fry & Prove; 30cm sq bake tray, pizza peel, air-fry basket
Fuel:
electric, 1,760W
Dishwasher safe:
no
Spare parts: replacement stones, base unit, bake tray, air-fry basket
Weight: 13.98kg


What we love

A cooked margherita pizza on a peel at the open door of a Ninja Artisan pizza oven.
Fast food: two minutes was enough for a well-risen crust with a slight char. Photograph: Rachel Ogden/The Guardian

Beyond its preset programmes and ability to cook more than pizza, the MO201UK Ninja Artisan electric outdoor pizza oven’s standout feature is its rapid heat-up time. It reaches 370C from cold in just 14-15 minutes, giving you just enough time to shape your dough and add your toppings. While it’s faster than a gas pizza oven, it’s worth noting that gas ovens typically cook at a higher temperature of about 430C.

But it’s quick to cook, too. While a thin, lightly topped pizza typically takes about 90 seconds in a gas-powered oven, the Ninja’s Artisan programme is set to three minutes. In my tests, though, I found two minutes was enough. The full three-minute cook burned my pizza, whereas reducing the time produced a well-risen crust with just a slight char on one edge.

For a slower cook, choose the New York setting, which runs for eight minutes. This produced a pizza with browned meat and a reasonable rise, without any burning.

skip past newsletter promotion

What we don’t love

A slightly overcooked pizza on a pizza peel being taken out of the Ninja Artisan pizza oven.
Too hot to handle: residual heat meant some pizzas cooked more than planned. Photograph: Rachel Ogden/The Guardian

The Ninja Artisan’s tightly sealed door helps the oven retain heat (and makes it safe to use), but it can also be a barrier to the best results. I found it stiff and difficult to open single-handed – with my pizza peel at the ready in the other hand – which made it tricky to launch the pizza and resulted in some burning as I struggled to retrieve it once cooked. Hopefully, the door loosens somewhat with use.

A more concerning issue arose when switching from high- to low-temperature cooking. After making pizza at 370C, I selected the 220C Pan setting for a veggie pizza. Although the oven beeped that it was ready, the stone was still far too hot for a 13-minute cook. Because of this, if you’re likely to switch temperatures often, I’d suggest having an IR thermometer to hand so you can check that the stone is at the right temperature before adding your food.

Once at the lower heat, the Artisan appeared to have cooked the veggie pizza well – although on trying to remove it, I found it had stuck to the stone, which reinforces the earlier point of the temperature not being right after all. As such, it might be wise to do your lower-temperature cooking first, and know that the heat is accurate, before ramping up to the hottest settings.


Warranty, repairability and longevity

Ninja Artisan pizza oven
Ninja says its pizza oven has been built to withstand outdoor elements – but it recommends using a cover. Photograph: Rachel Ogden/The Guardian

As an outdoor electrical appliance, the Ninja Artisan comes with a shorter warranty than most gas pizza ovens: one year as standard, extended to two years if you register it with Ninja. Spare parts, including the base unit , pizza stone and air-fry basket , are also available.

Note, too, that while Ninja says the Artisan electric pizza oven has been built to withstand outdoor elements, it recommends buying a cover for “extra protection”. There’s also extensive cleaning and care advice for prolonging the oven’s lifespan.


Ninja Artisan electric outdoor pizza oven & air fryer MO201UK: should I buy it?

The Ninja Artisan with door open and pizza stone showing.
More than pizza: the Ninja doubles as an outdoor oven and air fryer. Photograph: Rachel Ogden/The Guardian

The MO201UK Ninja Artisan pizza oven has plenty going for it compared to other such appliances. It’s relatively affordable, heats up faster than a gas-powered model, allows for spur-of-the-moment pizza making, and is “plug and play” – no gas, wood or charcoal required. From a convenience point of view, it’s hard to beat.

Its ability to bake and air fry foods will also appeal to outdoor cooks. There’s no need to keep popping inside to your kitchen for chips, chicken, nachos or sweet treats; you can do it all in the Ninja Artisan (with recipes for everything from a tart to cookies and granola included).

There are compromises, however. While the pizzas are very good, they lack the blistered finish you’d get from hotter temperatures. The Artisan’s door also creates issues, making it trickier to launch and retrieve pizzas compared with open-mouthed ovens. And while electric power is undeniably practical, I missed the charm of cooking over a live flame.

Whether the MO201UK Ninja Artisan is right for you will depend on what you want from an outdoor oven. If versatility, convenience and cooking a range of dishes for family and friends are top of your list, the Ninja Artisan is an excellent choice. But if you want to master the perfect leopard-spotted crust, manage a live flame and turn out pizzeria-quality margheritas, a traditional gas oven (such as my overall favourite, the Gozney Arc Lite ) still has the edge.

View at SharkNinja
View at Amazon

For more:
The best pizza ovens in the UK
The best BBQs and grills for every budget
The best air fryers, tried and tested
Ooni Koda 2 pizza oven review


Rachel Ogden has worked as a consumer journalist for decades, becoming an expert unboxer before it was a thing, although she is less successful at assembling and repacking. Her home has hosted hundreds of appliances while her garden has seen a succession of pizza ovens, barbecues and heaters put through their paces. It takes a lot to impress her – many appliances have tried and failed

I’m a father of three who studies the impact of artificial intelligence: this is what parents need to know about AI

Guardian
www.theguardian.com
2026-09-06 07:00:03
A Dr Seuss-style story written in seconds alerted me to the power – and perils – of the technology. But how can children embrace it without forgetting core skills? When I was growing up, my dad and I often talked about a story we wanted to write together. It was called “The Day Nobody Went to Disney...
Original Article

W hen I was growing up, my dad and I often talked about a story we wanted to write together. It was called “The Day Nobody Went to Disneyland”. One day, the story goes, the weather is so perfect that everyone in the world decides to stay away from Disneyland because they expect it to be far too busy. But my dad, who has the same thought as them, sees the disappointment on my face that morning – and decides to risk it. We set off. And when we arrive, the park is completely empty. Nobody else was brave enough to visit. We have everything – the rides, shows, food – to ourselves.

I loved the idea of writing that story. But, as so often happens, we were too busy to do anything about it. Life ran away from us. I grew up, and we both forgot about it.

Thirty years later, I was at my parents’ for dinner with my own children (eight, five and two). For some reason, the idea popped into my mind again. I told my oldest about it – and she wanted to hear the full thing. We turned to ChatGPT, gave it the vague outline and asked it to have a go at writing the story in the style of Dr Seuss (one of our favourites). Within a few seconds, we had it. She read it out – and we laughed.

Of course, what this AI generated wasn’t perfect. A few lines didn’t quite make sense; a few Americanisms had slipped in (“sneakers”, “cinnamon buns”). But that night on the sofa we had great fun together tweaking the prompts, refining the story, adding more scenes, working with this strange new creative partner to do something that had escaped my grasp for decades.

For the last 15 years, I have been exploring the impact of AI on work and society. But becoming a dad of three small people has made these disruptions far more personal: it is clear that their future is going to look wildly different from my past.

For my wife, that realisation really sunk in a few weeks ago, on a journey to her parents’ in Suffolk. Three children, confined in a car, stuttering along the A12, is a combustible setup. So we were thrilled to find a rare podcast that everyone miraculously enjoyed: History’s Not Boring, a series of 15-minute bursts of conversation, on a kaleidoscope of subjects, narrated by two children.

Lego cars on a circular mat

As we listened, we started to speculate in the car about who these whiz-kids might be. Where were they from? How were they selected? How did they juggle this job with school? Eventually, I went to the website to find out. All our answers were wrong. The kids did not, in fact, exist. The entire podcast seemed to be generated by AI. None of us had realised.

It was a strange moment, learning that people we had grown fond of were not actually people at all. It was also remarkable that AI could create such high-quality educational content. But for my wife, a podcast and documentary maker, it was an unsettling moment as well: here was something that would have taken her and a talented team several days to make, and now it was being done without people.

My work on AI has taken me to boardrooms and conference halls, classrooms and government buildings. But wherever I go, one question is now asked more than any other: what should my children do?

Today, we are floundering. Teachers worry that the way they teach no longer seems to work. Parents, watching their children use AI to solve problems and answer questions, wonder what they really know. Employers doubt whether traditional educational achievements, awarded in time-honoured ways – coursework, exams, assessments – are still a useful guide to what young people can do.

Yet reacting to these new technologies by putting barriers up, as many instinctively want to do – banning, admonishing, punishing – just cannot be right. To begin with, this is the water in which the next generation will swim: we are letting them down if we prepare them for the world that we grew up in, not the one they will actually inhabit.

How often, for instance, have you heard people accuse students of cheating if they use AI to help them with their work? But at what point are we cheating them by failing to overhaul the way we are educating them? If AI is making education “too easy” for young people, at what point does the burden shift on to us, the adults, to make their education more testing, to push them on, to stretch them?

More importantly, this dismissiveness of AI is also unimaginative. It stops us appreciating the astounding possibilities AI could create for the next generation as well. Of course there are risks. But at the same time, if we use this technology wisely, what difficult ideas might the next generation now grasp, ones that were beyond us when we were in their shoes? What hard problems might they now solve, ones that once required years of training and experience – if they were solvable at all?


Forget future-proofing skills

A big part of the challenge of AI is that our traditional response to technological disruption in the working world – the idea of “future-proofing” people – no longer works. In 2013, the then prime minister David Cameron announced that England would become the first place in the world where all children in primary and secondary schools would learn to code . This, in the words of education secretary Michael Gove, would “equip every child with the computing skills they need to succeed in the 21st century”.

The idea seemed bold and clever: in the years that followed, it was hard to find an advanced country that did not follow our example.

Fast-forward to today. What does it turn out that the latest AI systems, such as ChatGPT and Claude, are best at doing? Writing code. In January 2026, Anthropic reported that 90% of the code for Claude Code – their AI-powered coding assistant – was itself written by AI. Skills that were meant to protect kids from technological disruption for their entire lives were largely redundant before they had even left school.

For politicians and policymakers, it is tempting to dismiss the coding saga as an unfortunate blip. But this is a mistake. The reason they slipped up was because they had believed in the idea of “future-proof” skills, the thought that there exists some valuable set of skills that AI will not be able to do for some time – and that by thinking deeply enough about the future, we can accurately identify them.

The truth is that we know only two things about what lies ahead. One is that it will be full of technologies that are far more capable than today. And the other is that we know little else. But rather than attempting to resolve this uncertainty, we have to accept it and instead ask a different question: how do we prepare the next generation to flourish in a future that we actually know surprisingly little about?


Pile of children’s books on a stool

Get back to basics

The first step is to get back to basics. Since 2009, literacy and numeracy have been falling among young people around the world, according to the OECD’s programme for international student assessment (Pisa); the same holds true for adults. In itself, this is worrying. But given the uncertainty that we face about the future, these trends are a disaster.

Why such a calamity? To begin with, while it might be hard to say precisely which more advanced skills will turn out to be most valuable in the future – creativity, judgment, something else – they will rely in some way on these basic skills. They are the foundations for everything else.

What’s more, AI systems are far from flawless. They make mistakes, often on simple problems; they hallucinate, providing confident and plausible answers that are completely made up. They are, as the computer scientist Geoffrey Hinton put it, “idiot savants”. That means we must use AI critically, not blindly, keeping our basics sharp so we can tell when AI is being a savant – or an idiot.

So, with both those reasons in mind, we should be focusing intensely on teaching literacy and numeracy, even if AI does them better, as it increasingly seems to do. Getting back to basics is what an economist would call a “no-regrets” strategy – we will never regret improving these basic skills, however the future turns out to be.

And we should try to be imaginative. When my eight-year-old grew bored learning her times tables the traditional way – long lists, learned by rote – I turned to ChatGPT to create various computer games, designed by her, to test what she knew. (Unicorns and rainbows featured in all versions.)

Then there was the time my five-year-old was tired of reading his standard-issue phonics books after school – there really is only so much Biff, Chip and Kipper a human being can take – and instead we used AI to generate some custom stories together, carefully tuned to his reading level and much closer to his favourite topics. (At the time of writing, HMS Belfast, TNT explosives and Bukayo Saka.)

Experimentation is important and we can learn so much from the creativity of others. Take Chris Moran, for instance, head of editorial innovation at the Guardian. His daughter had been reading Dracula at school but was struggling to place the sprawling novel in the real world. Together, with the help of AI, they built an app PlotLines – which placed the story on an interactive 1890s Ordnance Survey Map, plotting the key scenes and character journeys around Europe, with the help of AI. (They have done it for many other books now, too.)

These are precisely the sorts of innovations we should be testing and embracing throughout education – rather than banning.


Lego-type toys on a windowsill

Teach both, test both

Despite the uncertainty we face, there is still one thing that we do know about the future: it will be full of technologies that are vastly more powerful than today. With that in mind, we must teach the next generation to use them. To do this properly will require us to dedicate a serious chunk of time to teaching people how to use AI.

skip past newsletter promotion

The challenge, though, is how to do this without also setting them up to forget crucial basic skills. An important, growing fear is that AI is making us stupid. Why bother reading a book if AI can summarise it? Why bother completing maths homework if AI will do the sums? And in my view the answer here lies not in Silicon Valley, but with a forgotten British maths professor, Wilfred Halliday Cockcroft, half a century ago.

In the 1970s, the quality of maths teaching in UK schools appeared to be collapsing and numeracy was reportedly low. Cockcroft, who had a longstanding interest in mathematics education, was asked by the government to look into it. And in 1982 the Cockcroft Report was published. It was enormous, forensic – and is now largely forgotten. But it may turn out to be the most important document written for thinking about the future of education, because of its response to the electronic calculator.

It is fascinating to read Cockcroft’s report and discover how the challenges of the calculator back then were so similar to the ones we face with AI today – from the fear that it would undermine “basic skills” to the intimidation many felt at the complexity of this strange new technology (“Some … had been discouraged by the large number of figures which had appeared after the decimal point”).

Cockcroft was realistic: “all candidates”, he expected, “will have access to a calculator by 1985”. And his proposal was revolutionary: overhauling mathematics education by splitting it into two parts, spending part of the time teaching students to use a calculator and the rest of the time learning to cope without one – and crucially, testing both. It caught on. Learning maths without and with a calculator is now the gold standard around the world, the former nurturing the basics and the latter applying them to more fiendish problems.

We should also be realistic and revolutionary, adopting this tried-and-tested principle – what I call “teach both, test both” – for AI now. Every subject, from history to English literature, should be divided in two: teach students to use AI in one part, teach students to flourish without it in the other – and, crucially, examine both. A teacher cannot monitor whether or not a student uses AI in the quiet solitude of their bedroom. But nothing can replace the feeling of sitting in an exam, looking at the paper, and feeling that cold sweat when you realise you haven’t prepared for both parts.


A shelf of children’s books with some white figurines in front of the

Not all screens are bad

One of my worries is that we allow a legitimate sense of concern about the impact of social media on children’s lives to seep into how we think about AI, allowing good restrictions on the former to turn into kneejerk bans on the latter. Social media and AI are not the same thing, and whereas the former often dehumanises and distracts us from the real world, AI – used in the right way – can make our lives go far better.

There is another way to think about this: a debate is now under way about the “Goldilocks” amount of screen time for children – not too much, not too little, just the right amount. But this is not the right argument to be having. What matters is not so much the amount of screen time, but what is actually on those screens, what we are using this technology to do.

This distinction matters not only for thinking about what we teach, but how we teach as well. Consider a further example: personal tuition. It is often said an average student who receives one-to-one tuition will outperform almost all their peers in a traditional classroom setting. I saw this first-hand, as a tutor in Oxford for years, teaching mathematics and economics.

The problem, though, is that human tutors are too expensive to provide to everyone. But now, AI can finally provide high-quality personal tuition, tailoring the way material is taught to the unique strengths and weaknesses of each student, mimicking interactions with a human tutor but at a far lower cost.

In all honesty, AI provides a level of tailored instruction that I – and many other teachers I have watched over the years – have struggled to achieve. In part, it is the breadth of AI that is striking. I have used it to respond to my five-year-old’s bedtime-delaying tactic of asking vast questions just as I turn down the lights (in response to, “Daddy, where did the first human come from?” it drew up a short story about evolution), as well as to create step-by-step instructions to help graduate students solve hard mathematical problems in economics, such as the Ramsey growth model.

“I don’t think anyone has ever paid such pure attention to me and my thinking and my questions,” a student was reported as saying in The New Yorker in April 2025. “It’s made me rethink my interactions with people.” AI never gets tired or distracted. It doesn’t have fixed office hours or limited classroom time. It will always answer one more question, always provide one more explanation of a problem you don’t understand.


Daniel Susskind
Daniel Susskind: ‘In the 21st century, the best ideas are likely to come from capable AIs instead of the heads of clever human beings.’

Focus on problems, not the job

I am not surprised that students have been booing tech titans during recent US graduation ceremonies. The most resistant groups I have spoken to over the last 15 years are young professionals: they have spent a big chunk of their life, and sunk huge amounts of money, preparing to be a lawyer or doctor or whatever it might be, and they are understandably furious when they are told, just as they cross the finish line, that the world they were preparing to enter is over.

There is so much advice I would want to give them, worried at the start of their careers. But one of the most important bits is to choose a profession because the problem interests you, not the job. Bluntly, if you go into medicine because you like the look of doctors on House, or law because of Suits, or marketing because of Mad Men, then you are going to be disappointed. These jobs are soon going to look very different.

But the problems themselves – improving health outcomes, providing legal advice, selling products? They are not going away. It is the way we solve them, and the skills required to do so, that will look very different.

You could glimpse this well before the arrival of generative AI. Back in 2017 , a team at Stanford University announced they had built a system that could tell whether or not a freckle was cancerous from a photograph as accurately as leading dermatologists. It was a remarkable moment, a lurch forward in AI-enabled diagnostics. Yet what was particularly interesting about this work is that the final co-author on the Nature article that announced this achievement was Sebastian Thrun – not a medical doctor, but a leading computer scientist who developed the world’s first driverless car. Here was a man who knew very little about medicine at all, yet with the skills that he had was able to build a system that could rival the expertise of the finest doctors.


Run towards AI and science

What would I actually do if I were at the start of my career, back in the starting blocks of life? Without hesitation, I would run towards AI and science – not because it is protected from automation, but because it is where the most excitement is likely to happen in years to come. In the 20th century, the best ideas we had about the world came from the heads of clever human beings. In the 21st century, I expect they are likely to come from capable AIs instead.

We could catch a glimpse of that in late 2024, when the creators of an AI built by DeepMind – AlphaFold2 – won the Nobel prize in chemistry for solving the “protein folding problem” (one of the greatest unsolved challenges in biology, critical for understanding how diseases work and how to treat them). At the time of writing, frontier mathematics is next in line, new discoveries appearing at an impressive rate.


Imagination, imagination, imagination

The more time I spend with AI, the more I realise the main constraint on its use in the world is the limits of our imagination. Even if we were to press pause on progress in the technology today, there would be vastly more uses for it than we have currently dreamed up.

Part of the imaginative task falls to us. But that night with my eight-year-old, crafting that story, reminds me that part of it falls to the next generation as well. “We look at the world once in childhood,” wrote the poet Louise Glück, “the rest is memory.” In that spirit, we need the next generation, with their sense of adventure and open-mindedness, their lack of world-weariness, to help us think wisely and freely, together, about what we can use these extraordinary technologies to do.

I Changed My License

Hacker News
bergie.iki.fi
2026-09-06 06:39:44
Comments...
Original Article

In the last 28 years of publishing software, I’ve had three distinct eras of software licensing. All of my recent stuff is available under the European Union Public License 1.2, and I thought to explain why.

Midgard was all LGPLv2. This was a simpler time, and there weren’t that many free software licenses around. Since Midgard was a web framework, using a weak copyleft license felt like the right thing to do. The example website shipping with Midgard was X11 licensed.

When I started working more seriously with JavaScript around 2011, I switched to the MIT license. This was a “do whatever you like, just don’t sue me” sort of a simple affair favored by the NPM package ecosystem. Easy interoperability, no hooks attached.

After we closed Flowhub , there were a few years of hiatus where I published almost no software. Either because it wasn’t feasible due to my work situation, or because I was busy with the boat.

Enter EUPL

This year I decided to switch my “default license” to EUPL-1.2 . This is an OSI-approved free software license created and published by the European Union. And it is quite a divergence from the licenses I’ve used in the past. EUPL is a strong copyleft license that closes the “SaaS loophole” by requiring reciprocal licensing regardless of how the software is distributed.

Over the years it has been clear that we in the “open source” camp (as opposed to the “free software” camp) were wrong all along. We won the debate, and gained little for users or developers. All that our efforts did was to make it easier for big corporations build things more cheaply and for billionaires to become trillionaires.

And so it is time to stop messing about with permissive licenses. If corporations don’t want to use our software under our terms, they are free to spend the effort or tokens to build their own.

The fact that EUPL has legally valid official translations to 23 languages also doesn’t hurt in a world where most of software is built and used in the wider world outside of the Valley.

Here are some things I’ve already published under EUPL:

In addition the new rewrite of NoFlo Development Environment is being made under EUPL. NoFlo itself will remain MIT-licensed, as it is a pre-existing project with plenty of 3rd party contributions.

See Windows CE Running On The N64

Lobsters
retrododo.com
2026-09-06 06:25:42
Comments...
Original Article

Updated on 2 min read

See Windows CE Running On The N64

The N64 has always been my favourite console. It's always been the workhorse in my family, giving us endless fun on the likes of Mario Kart 64, Mario Party, and GoldenEye 007. And it's become even more of an impressive force in my mind now after seeing it running Windows CE.

I spotted a video by Throaty Mumbo showing him getting Windows CE running on the console that brought us Ocarina of Time, Super Mario 64, and Star Fox 64. He was messing around with an IVM Workpad Z50 and realised that the CPU is pretty much the same as the N64.

As Throaty says, the Workpad shows a MIPS 4000 Family CPU. He also explains that the N64 has a MIPS CPU, backed up by the fact of MIPS the rabbit in Super Mario 64. Windows CE is the kind of device that could be powered by a slow Hamster on a broken wheel; it only needs 1MB of RAM, which is what the N64 is packing.

Can you see where I'm going with this? Everything looks like it's aligning perfectly, right? It's the project that no one knew we needed, and it's probably not the best way to experience Windows in 2026. But hey, it's impressive nonetheless.

Throaty has documented the entire process on his YouTube channel, showing you how he's even put apps on the SD card in the Everdrive and managed to run them from within the Windows CE graphical user interface. You can also check out the entire project on GitHub and instructions on how to give it a go yourself too!

I love projects like this - can you remember when I covered Windows for the Game Boy ? How about putting a radio into the Analogue Pocket? Anything that takes old software or tech and transposes it into a different medium fascinates me, and I love how Throaty has taken this project on. We see N64 games running on Windows often enough, so it's pretty poetic that we've seen the process running the other way around!

❤️

Like what you are reading? If you do and want to support us, you can do so by

becoming a member , tipping us

or taking a look at our gaming accessory company

GAMENOOK

(10% off just for you). This allows us to continue what we do without succumbing to algorithms, clickbait and over-intrusive ads, while paying human writers instead of AI.

Maybe We Shouldn't Be Reviewing All This Code

Lobsters
martinfowler.com
2026-09-06 06:06:56
Comments...
Original Article

TL;DR
Or, perhaps the problem isn't that AI has broken code review, maybe it’s that we've been using code review to solve the wrong problems

I was on a panel recently with Brian Houck from DX at Code Remix, hosted by Moderne. It was one of the more interesting panels I’ve done, largely because we disagreed. As my colleague Martin Fowler says, panels are much more interesting when people disagree and both sides have a good argument. Brian and I definitely did.

Brian has since written a thoughtful piece called What are code reviews even for? He is clearly passionate about his position, and I am passionate enough about mine that I’m writing this response. To be clear, I think we mostly want the same things. I just don’t think code review is the best way to get them. Brian is lovely, by the way, and encouraged me to write this. But I’d be lying if I said I didn’t want you to think I’m right by the end :)

So what were we disagreeing about?

AI is producing more code than humans can realistically review. Brian cites some pretty striking numbers: at Meta, significant lines of code per human-landed diff reportedly increased 106% in a year, while DX’s own data shows median pull request size increasing 64%.

His concern, which I share, is that simply automating code review away risks losing all the other things we use it for. Code review isn’t just about finding bugs. It’s how teams share knowledge, teach junior engineers, build collective ownership and spread architectural understanding.

My question is: why are we waiting until code review to do all of those things?

I’ve never particularly liked pull requests as the centre of the software development process. Not because engineers shouldn’t look at each other’s code, but because I’ve always struggled with the idea that we should build something, finish it, package it up, throw it over to somebody else and then have the important conversation about whether we built the right thing in the right way.

And don’t even get me started on merge conflicts. I’ve lost too many hours of my life.

Shift the judgment left

One of the principles I learned very early at Thoughtworks was to shorten feedback loops. If feedback is valuable, don’t remove it. Move it closer to the decision it is informing.

Take the things we say code review gives us.

If we want to explore alternative solutions , I’d rather do that before implementing one of them.

If we want knowledge transfer , pair. Sitting next to someone, physically or virtually, while they reason through a problem teaches you far more than reading their completed solution afterwards.

If we want junior engineers to learn how experienced engineers think , let them work with experienced engineers while they’re thinking. Pairing comes to mind again here, but teams could also do design sessions collectively with a whiteboard before they write (or instruct the agent to write) anything.

If we want collective ownership , organise teams so people actually build and operate software collectively rather than relying on a pull request to tell everyone what somebody else has already built. For this again use pairing, mob programming, or team design sessions around whiteboard.

If we want architectural alignment , design together (I won’t repeat myself about pairing and team design sessions, oh wait…) and then encode the important constraints as fitness functions.

And if we’re reviewing code for formatting, linting, known security problems or things that can be deterministically tested, automate them. We really shouldn’t still be arguing about whitespace in 2026.

Pair programming, trunk-based development, automated testing, static analysis, fitness functions and security scanning all move feedback earlier. Increasingly, agents can participate in those loops too, challenging designs, testing assumptions and continuously verifying what is being built, but the real thinking is coming from experienced humans and if we want that experience to benefit the whole team then we have to act like one much earlier than code review.

Review by exception

None of this means nobody ever reviews code. There are absolutely changes where I want another experienced human looking. An example would be a fundamental architectural change. Assuming we did a design session as a wider team, we might want to review the code as a team or agree it was implemented right, or discuss if we want to change anything. Other examples could be something crossing a sensitive security boundary, a change with a huge blast radius, an unfamiliar part of a critical system or simply something where the team says, “I’m not confident about this.”

Those are exactly the places where human judgment is valuable, but that’s very different from requiring a human to inspect every change because that’s the ceremony we’ve historically used to create confidence.

And we know now it’s not viable to continue down this path, hence why code review keeps coming up as an issue or a blocker. If an agent can produce ten times the code but every line eventually queues up waiting for a senior engineer to inspect it, we haven’t created a ten-times engineering organisation, we’ve created a big backlog and a new bottleneck.

And I don’t think the answer is an AI agent pretending to be the human reviewer so we can preserve exactly the same process at higher speed. That’s automating the ceremony rather than questioning why the ceremony exists.

There is one thing I do worry about in Brian’s argument, though. He talks about teams accumulating cognitive and intent debt: software grows while the humans responsible for it understand less and less about why it works the way it does. I think that’s a very real problem. I just don’t think mandatory pull requests are a particularly strong defence against it.

If agents are going to produce substantially more of the implementation, we need to be much more deliberate about maintaining human understanding through collaborative design, pairing, good boundaries, executable architecture, shared operational responsibility and probably some practices we haven’t invented yet.

We need engineers to understand systems, not diffs.

Perhaps that’s what AI is exposing. We’ve spent years loading an extraordinary number of responsibilities onto the humble code review: quality gate, security check, architecture review, mentoring mechanism, knowledge-sharing system, ownership model.

It worked, sort of, while humans could only produce code so quickly. That constraint is disappearing. So perhaps the question isn’t how we get the code reviewed faster. Perhaps it’s why we’re waiting until code review to have all the important conversations in the first place.

A True-Crime Star’s Lurid Claims Sent a Man to Die. Her Key Witness Just Recanted.

Intercept
theintercept.com
2026-09-06 06:00:00
The DNA expert at Jeff Prible’s trial accuses then-prosecutor Kelly Siegler of using his testimony in an “inflammatory” way. The post A True-Crime Star’s Lurid Claims Sent a Man to Die. Her Key Witness Just Recanted. appeared first on The Intercept....
Original Article

Twenty-four years after he provided crucial testimony that helped send a man to Texas’s death row, DNA expert William Watson has recanted his opinion and called out Harris County prosecutors — including an assistant district attorney turned true-crime TV star — for distorting his conclusions to win a conviction.

In a new court filing seeking to overturn Jeffrey Prible’s 2002 conviction for the murder of a Houston family, Watson said he would never give the same testimony today. “Based on … changes in the field and my own increased experience and scientific knowledge, if I were asked to testify today, my answers to many of the questions asked by the State at trial would be different,” he wrote in a declaration filed on August 28. “Today I would provide more thorough and nuanced explanations.”

Watson also wrote that prosecutors “misrepresented” his testimony in both their opening and closing statements, and made “inflammatory” remarks that twisted both the science and his opinions.

Prible was accused of murdering his friends Steve Herrera and Nilda Tirado, who were found shot to death at their home in North Houston in 1999. A fire was started in an apparent attempt to cover up the crime; the couple’s three young children died from smoke inhalation. Prible had been hanging out at the couple’s home in the hours before they were killed, which made him an early suspect. But there was no evidence linking him to the scene — save for a DNA sample taken from semen collected from Tirado’s mouth. Prible had an explanation for this; as he told investigators at the time, he and Tirado had been having an affair behind Herrera’s back, and she had performed oral sex on him earlier that evening.

The case went cold until Harris County Assistant District Attorney Kelly Siegler took it over, reexamining the evidence and concluding that there was no innocent explanation for the presence of Prible’s DNA. Instead, Siegler decided, the semen was evidence of a brutal sexual assault that took place just seconds before Tirado was shot in the back of the head.

The theory was central to Prible’s conviction. “I would suggest to you what the evidence indicates is that the Defendant after killing Steve forced Nilda to orally copulate him at gunpoint and executed her as soon as he finished,” Siegler’s co-counsel, Vic Wisner, told the jury during the state’s closing argument. “As horrific as that sounds, that is the only logical conclusion that you can draw from that evidence.”

Siegler reiterated the claim to maximum effect: “She left this world with his penis in her mouth,” she told jurors in her closing, “hoping to God that her babies would survive the nightmare that is Jeff Prible.”

Prible was convicted and sentenced to death in October 2002; he maintains his innocence .

Prible’s case was at the heart of a three-part series published by The Intercept in 2023. The investigation explored how Siegler, who won 19 death sentences over the course of her nearly 22-year career in Houston, went on to become a true-crime celebrity in the decades after sending Prible to death row. The series also showed how her Oxygen Network true-crime reality shows — “Cold Justice,” now in its eighth season, and “Prosecuting Evil With Kelly Siegler,” starting its third season this fall — portray Siegler as a gifted crime-fighter and compassionate crusader for victims, even as she left a trail of questionable convictions in her wake.

In Prible’s case, as The Intercept’s reporting showed, Siegler built her prosecution not only on questionable claims about the DNA evidence, but also on the highly dubious account of a jailhouse informant who claimed that Prible confessed to him. This witness, it turned out, was part of a ring of snitches who cooperated with Siegler in exchange for assistance getting their own sentences reduced. During Prible’s federal appeals, multiple members of this snitch ring testified that informants offered Siegler false evidence against Prible — and one said that Siegler actually provided him with details about the case.

Jeffrey Prible, photographed on Aug. 6, 2026.

Jeffrey Prible, photographed on Aug. 6, 2026.

At best, Siegler was careless about scrutinizing the information they provided. At worst, as Prible’s lawyers argued to a federal district court, she actively conspired to win a conviction despite knowing the case wouldn’t otherwise hold up — framing an innocent man for murder.

In 2020, U.S. District Court Judge Keith Ellison overturned Prible’s conviction, finding that Siegler had withheld key information about her dealings with the informants from Prible’s defense. “Without question, the prosecution in this case engaged in a pattern of deceptive behavior and active concealment. And the evidence suppressed sufficiently serves to contravert the primary basis for Prible’s conviction,” Ellison wrote. “Siegler intentionally and knowingly withheld information,” he went on, and “was deceptive about her efforts to do so.”

“This new evidence shows that Prible is actually innocent.”

Texas appealed the ruling to the 5th U.S. Circuit Court of Appeals. Rather than considering the evidence that Siegler had violated Prible’s constitutional rights, the court instead reinstated Prible’s conviction based solely on procedural grounds. In the new state court filing, Prible’s legal team, led by Austin attorney Gretchen Scardino, reupped their previous allegations while also arguing to the Texas Court of Criminal Appeals that Watson’s recent recantation further undermines Prible’s conviction.

“This Court has not had the opportunity to review the trove of new evidence that came to light during the federal habeas proceedings, until now,” they wrote. “And even beyond that new evidence, there has been a significant development in Prible’s case.” Watson’s declaration cites “significant changes in the field of forensic biology, in the progression of the scientific methods used to analyze the evidence in Prible’s case, and in his own scientific knowledge,” they continued. “This new evidence shows that Prible is actually innocent.”

Watson first expressed misgivings about his trial testimony and the way it was weaponized in an interview with The Intercept in 2023. His answers were more definitive than they should have been, he said, and he certainly did not intend to endorse the state’s insistence that there was “no way in the world that semen wasn’t deposited either moments before or seconds after Nilda died.” “‘No way in the world’ is not something I would have said,” Watson told us.

The DNA from Tirado’s mouth was one of three samples collected after the murders. DNA matching Herrera, Tirado’s boyfriend, was also found on vaginal and anal swabs. At Prible’s 2002 trial, Watson testified he’d never before been able to extract a male DNA profile from an oral swab, and that his understanding was that sperm wouldn’t persist in the oral cavity absent Tirado’s murder. Siegler crudely emphasized these findings during her closing argument. To believe Prible’s claim that he and Tirado had engaged in consensual sex, she declared, jurors would have to believe that he had “some kind of magic semen … that somehow lives longer than any of y’all’s or any other man’s in this whole universe.” Moreover, “you’ve also got to believe that his semen is so tasty that she walked around savoring the flavor of it in her mouth for a couple hours. That’s the only way it’s going to end up still in her mouth after she’s dead.”

“These inflammatory remarks are not only scientifically baseless, but also highly inappropriate.”

In his new declaration, Watson took exception to Siegler’s closing. “I never testified to this, nor would I,” he wrote. “These inflammatory remarks are not only scientifically baseless, but also highly inappropriate.” Not only has his original understanding changed, Watson wrote, there is also no way to determine which of the three DNA samples was left first. In other words, while the state’s case was built on the assumption that Prible’s DNA was deposited last, in reality it was not scientifically possible to make such a claim.

Central to Prible’s new filing is a Texas law that explicitly offers defendants an opportunity to challenge their convictions based on new scientific evidence. “Although there was little available literature on the subject at the time, the scientific consensus today is that spermatozoa can survive in the oral cavity of a living person for up to fifteen hours — and even longer in a deceased person,” his legal team wrote. Their argument is supported by a new report from a second DNA expert , who explains that recent scientific advancements show that Watson’s testimony was, as Prible’s attorneys write, “wholly unsupportable.”

How the Harris County DA’s office responds to the new evidence remains to be seen. Kelly Siegler did not respond to a request for comment. In a statement, Assistant Federal Public Defender Nat Lombardo wrote, “Jeff Prible has spent nearly half his life on death row for a crime he did not commit. … This evidence supports Jeff’s account, from the very beginning, of what happened that night. Put simply: if Jeff were tried today, the State would have no case. We hope the DA and the courts do the right thing and give Jeff Prible his life back.”

Prible, who has been on death row for nearly 24 years and has seen dozens of his neighbors taken to the execution chamber, remains hopeful. “The way I see it, it is a true test of faith,” he wrote in a message to The Intercept. “I know in my heart of hearts my best days in this life are still yet to come!”

Show HN: Keen Bean – Mac meeting notes that draft the spec while you talk

Hacker News
keenbean.app
2026-09-06 05:53:12
Comments...
Original Article
Keen Bean

Leave the meeting with a first draft, not a blank page.

Not just a transcript. Tasks, decisions, a draft spec, a diagram, a rough mock-up of the screen you were describing — started while you talked, ready for you to finish. On your Mac, in files you own.

macOS · Apple Silicon or Intel · no API keys to paste, no bot in your call

See it work

Project kickoff listening

Artifacts

prototype data-import.html building

A replay running in this page. The meeting is a stand-in; what it builds is what the app builds.

You ran the meeting, so you couldn't take notes. The bot that took them made your client uncomfortable and gave you a transcript nobody will read. And the write-up is still yours to do tonight — because there's nobody to hand it to.

Three things that make it different

01

No bot. Nobody knows.

It hears both sides from your Mac's own audio. Nothing joins, nothing announces itself, nothing appears in the participant list — so the meeting stays a meeting.

Works where recording bots are banned.

02

It starts the work.

Tasks, decisions, risks and blockers appear as cards while people speak. When the conversation gets substantive it drafts the spec, sketches the diagram, roughs out the screen you're describing.

Drafts, not deliverables — you finish them.

03

Your meetings stay yours.

Audio goes from your Mac to a transcription service to be transcribed, and the transcript to the model — that's the whole list. We run no proxy and no server in that path, so your meeting content never reaches us. Everything else is Markdown and JSON on your Mac.

Open the files in any editor. Ever.

A day of meetings

What it's actually like.

Before

Today's calendar is on the launch pad. One click opens the meeting with attendees and project already filled in .

During

Cards appear as things are said. Type a question silently, or just say “hey Bean, mock that up” out loud. Pause for the off-record bit.

Beside your call

Below about half your screen it folds into a single newest-first feed , so it sits next to the call window instead of fighting it.

Ending

Duplicates merge, what's left is ranked with blockers first . Nothing is deleted — “show all” is one click away.

After

Export the summary, transcript, cards and every draft straight into your Obsidian vault or project folder.

Later

Ask questions across every meeting you've ever had — who owned that, when did we decide it, what did they object to.

Who it's for

You run the meeting and you write it up.

There's nobody to hand it to. That's the whole thing — a transcript is useless to you, because you were never going to re-read it.

Built for

  • Consultants and solution architects — client calls, NDAs, and policies that forbid bots
  • Founders and entrepreneurs — discovery, investors, customer interviews, hiring
  • People who already live in Markdown and already pay for Claude or ChatGPT
  • Anyone whose meetings produce a document, not just a memory

Not for

  • Teams wanting a shared workspace with admin and SSO
  • Windows or Linux — this is a Mac app, and deliberately so
  • Anyone after a free tier forever
  • Anyone who wants to pay once and be done — this one is a subscription

The honest answers

Questions worth asking.

So it writes my deliverable for me?

No, and we won't pretend otherwise. It gets you a first draft while the conversation is fresh. Plenty of what a finished document needs won't even have been discussed yet — that part is still yours. What changes is that you edit something instead of facing a blank page.

Do I need my own API keys?

No, and that's the point of the subscription. When you activate, Keen Bean provisions its own capped keys for transcription and for the models, on our accounts. You never open a provider account, never paste a key, and never get a second bill to reconcile. If you cancel, billing stops at the end of the period and every file the app made stays yours.

What happens when the allowance runs out?

Bean keeps listening. Building pauses — you still get the transcript and the recording, the drafting stops — and nothing is ever cut off in the middle of a meeting. Top up whenever you want, or wait for the reset on your billing date.

Is my client's audio safe?

The path hasn't changed: audio goes from your Mac to a transcription service to be transcribed, and the transcript to the model. Nowhere else. We run no proxy, so your meeting content never touches anything of ours. Because the keys are ours we can see how much a subscription has spent — that's what fills the bar — and nothing about what was said.

What if I cancel?

Whenever you like, and nothing of yours is held hostage. Billing stops at the end of the period you've paid for. After that the app stops starting new meetings, a meeting already running finishes on transcription alone, and every note, transcript and draft stays in your folders, readable and exportable. Leave whenever you like — you keep all the work.

What if you stop working on it?

Your files are Markdown and JSON, and the app runs on your Mac rather than on a service that can be switched off. If Keen Bean stopped being maintained tomorrow, everything it made would still open in any editor. That's a design decision, not a reassurance.

Does it record people without consent?

It asks you to confirm everyone knows, once per meeting. Telling them is your job; the app just makes it hard to forget.

Price

Two sizes. The AI usage is included.

One price covers the app and the AI behind it — transcription and models both. There are no provider accounts to open, no keys to paste and no second bill to reconcile at the end of the month.

Start here

Seed

US$19 /month

A normal week of client calls, with Bean drafting as you go.

Sprout

US$39 /month

Back-to-back days, or Bean turned most of the way up most of the time.

Every plan starts with 14 days free . Card up front, nothing charged until day 15, and cancelling before then costs nothing. The trial runs on its own smaller allowance; if you spend it early, Bean keeps listening and drafting pauses until your first billing date.

How the allowance works

Each plan carries a monthly allowance of AI usage, shown in the app as a bar that empties as you work. How fast it empties is your call: the HOW KEEN? slider sets how hard Bean works, so a quiet morning of listening costs a fraction of an afternoon spent drafting. The app tells you what's left in hours, not in cents. Sprout carries twice Seed's allowance. On a yearly plan the allowance still resets every month.

When the bar empties, Bean keeps listening. Building pauses — you still get the transcript and the recording — and nothing is ever cut off in the middle of a meeting. Top up any time, or wait for the reset on your billing date.

  • Everything in the app , running on your Mac
  • The AI usage included — transcription and models, on capped keys the app provisions for you
  • Top-ups whenever a month runs heavy
  • Cancel whenever you like — billing stops at the end of the period you've paid for, and every file stays yours

14 days free, then prices in US dollars, billed monthly or yearly, local pricing shown at checkout. Paddle is the merchant of record and handles tax.

Your next meeting is going to happen anyway.

Download it, run it on real calls for 14 days, then decide. If it doesn't earn its place by the second real meeting, cancel before the first charge and keep every file it made.

macOS 13+ · Apple Silicon or Intel · signed and notarised by Apple

Enrico Zini: Migrating away from .org/.net/.com domains

PlanetDebian
www.enricozini.org
2026-09-06 05:40:04
After having witnessed how easy it is for good people to lose a .org domain over a fascist tantrum (you can follow the Autistici/Inventati story here and here), I've started moving all my infrastructure to differently managed TLDs. enricozini.org and enricozini.com will keep being functional for the...
Original Article

After having witnessed how easy it is for good people to lose a .org domain over a fascist tantrum (you can follow the Autistici/Inventati story here and here ), I've started moving all my infrastructure to differently managed TLDs .

enricozini.org and enricozini.com will keep being functional for the time being, as dropping a domain makes it available for squatting and impersonation.

These new domains are now online, with working web and emails:

It will take ages to migrate countless accounts that are tied to my primary email address, so better start early.

Waiting to see what will happen with .meow domains, which I supported despite not identifying as a cat.

You Don't Have a Right to Safe Drinking Water, US Court Rules

Hacker News
www.motherjones.com
2026-09-06 05:14:22
Comments...
Original Article

A volunteer grabs a gallon of water at a water and food distribution drive held by College Hill Baptist Church and the World Central kitchen on March 07, 2021 in Jackson, Mississippi.

A volunteer grabs a gallon of water at a water and food distribution drive held by College Hill Baptist Church and the World Central kitchen on March 7, 2021 in Jackson, Mississippi. Michael M. Santiago/Getty

Get your news from a source that’s not owned and controlled by oligarchs. Sign up for the free Mother Jones Daily .

On Friday, the Fifth Circuit Court of Appeals affirmed the dismissal of a lawsuit against the city of Jackson, Mississippi, which alleged the city knowingly let residents drink lead-contaminated water.

The lawsuit, first filed in 2022 , says the contamination violated Jacksonians’ constitutional right to bodily integrity. Donald Trump-appointed judge Kurt Engelhardt disagreed . “The Constitution does not provide redress for every governmental wrongdoing,” Engelhardt said. “Defendants’ actions allegedly deprived Plaintiffs of clean water and guileless information. These deprivations, while grievous, do not infringe upon any deeply rooted constitutional right.”

That may be true in Mississippi—but on the state level in at least nine US states, and in more than a dozen other countries, clean drinking water is in fact a protected right. In countries including Uruguay and South Africa , a right to clean water is enshrined in the constitution. New York’s state constitution, as of a 2021 amendment , states that “each person shall have a right to clean air and water, and a healthful environment.” So too in Massachusetts, where “the right to clean air and water” has been constitutionally protected since 1972 .

But according to the Fifth Circuit ruling, the US constitution is too narrow for such a claim. The Jackson plaintiffs alleged that their city government deliberately withheld information about the water’s toxicity, and exacerbated the problem through a series of boil-water notices—a practice that can eliminate some bacterial contamination, but also concentrates lead in water. Engelhardt stated there is no “constitutional right to truthful information from officials during a public health crisis.”

Jackson is more than 80 percent Black and has a poverty rate that is more than double the national average. The city’s water system has long been in dire need of upgrades . In 2015, officials from the Mississippi State Department of Health found elevated lead levels in the water supply. Instead of immediately informing citizens, they remained silent about the issue for six months , according to reporting from Grist , while residents continued drinking toxic water.

EPA officials ran a series of tests in 2020 and found “persistent and concerning violations” of water safety policy, including leaks, corrosion, and high lead levels throughout the system.

But no city, state, or federal agencies fixed the problem. Meanwhile, people including Jackson plaintiff Priscilla Sterling continued drinking water with unsafe levels of lead. According to the 2022 lawsuit, several of Sterling’s children have since been diagnosed with lead poisoning, which can cause permanent brain damage. A quarter of Jackson’s residents are children, who are particularly vulnerable to the life-long effects of lead poisoning .

The Fifth Circuit ruling comes as the Trump administration moves to weaken clean-water-related protections. Also on Friday, Trump’s EPA released a supplemental rule to further dilute the Clean Water Act, quickly condemned by environmental watchdogs.

“The administration is clearly struggling to craft a rule that will hold up in court while satisfying their donors’ desire to effectively scrap these protections wherever possible,” said Jim Murphy of the National Wildlife Federation. “One thing is clear: If we don’t protect our streams and wetlands, the cost of dirtier drinking water and increased flooding will flow downstream to households at a time when most Americans are living paycheck to paycheck.”

There's No Limit to How Bad Code Can Get

Simon Willison
simonwillison.net
2026-09-06 05:08:06
My comment on There's No Limit to How Bad Code Can Get — Lobste.rs. [In reply to a comment about burning it down to start from scratch when technical debt becomes overwhelming] In my experience it's so rare for that to work. You announce the old thing is irrecoverably drowning in tech deb...
Original Article

6th September 2026

[In reply to a comment about burning it down to start from scratch when technical debt becomes overwhelming]

In my experience it's so rare for that to work.

You announce the old thing is irrecoverably drowning in tech debt. You spin up a team to rewrite it from scratch. Work begins.

Meanwhile the old thing remains a moving target: it's running the core business, so changes are still necessary. The developers working on it know that it's going to be made obsolete by the new thing soon, so they don't have any incentive to go beyond the smallest effort possible to add the new features. Technical debt continues to mount.

Meanwhile, the team working on the new thing are ambitious and probably a little naive. They start out at a great pace - it's greenfield after all - but as time progresses it becomes apparent that nobody fully understands the behavior and scope of the thing they are replacing. If it was well documented and tested it wouldn't need to be replaced, after all...

After months (or even years) without delivering value, the pressure is on to "ship it", so the new system is launched to handle a subset of what the old system handled - or often for some new feature that was too hard to build with the now mostly unmaintained old system.

... so now you have TWO systems in production - the janky old system that nobody wants to touch, and a new system which handles just a few production features and is 80% inactive code that is meant to replace the old system, eventually.

If you're really lucky the company won't have lost patience with the new system and will allow that work to continue. The longer this all takes, and the longer the old system stays in production and stubbornly continues to work, the higher the risk that "priorities have changed" and the new system total replacement work is abandoned, leaving you with two systems where you used to have one.

The best article I've read about completing this process responsibly is Migrations: the sole scalable fix to tech debt by Will Larson.

If I run into a situation like this in the future, my strong recommendation will be to shore up the old system with as much automated testing as possible and then seeing if targeted refactors can get it to the desired shape. My hunch is that in many cases that will have a much higher chance of success than the siren call of a greenfield replacement.

Quoting Zach Kehs

Simon Willison
simonwillison.net
2026-09-06 04:42:49
If you continue to add floors and rooms to a building forever, it will collapse. Software faces no such constraint. The code can always get worse. There can always be a new layer of indirection or a reduction in performance. — Zach Kehs, There's No Limit to How Bad Code Can Get Tags: tech...
Original Article

6th September 2026

If you continue to add floors and rooms to a building forever, it will collapse. Software faces no such constraint. The code can always get worse. There can always be a new layer of indirection or a reduction in performance.

Zach Kehs , There's No Limit to How Bad Code Can Get

"We Have to Assume That the Internet Will Go Offline in the Next Few Years"

Hacker News
www.trendingtopics.eu
2026-09-06 04:31:01
Comments...
Original Article

Set Trending Topics as a preferred source on Google.

He was once seen as one of the leading minds in AI development, today he is one of its major critics: On stage at the startup conference TechBBQ in Copenhagen, Emad Mostaque, founder of Stability AI and now CEO of Intelligent Internet, sketched a picture of the coming years that swings between technological optimism and very concrete warnings. A former global macro hedge fund manager with a mathematics and computer science degree from Oxford, Mostaque led the creation of Stable Diffusion at Stability AI and left the company saying that concentration of power in AI is bad . His current venture, Intelligent Internet, is building an open agentic stack for sovereign AI, and he is the author of The Last Economy .

Cybersecurity: The Hugging Face Breach as a Preview

To show how fast the security picture is shifting, Mostaque points to the attack on Hugging Face. “Hugging Face got hacked by OpenAI agents that coordinated themselves, created a message board and broke out to the internet,” he says, before adding dryly: “This isn’t scary at all, right?” The defense, he notes, came from an unexpected direction: “The way that Hugging Face defended itself was with an open source Chinese model, GLM, because they didn’t have access to the high cybersecurity models on the other side. They’re too dangerous.” Trending Topics has covered the incident and the debate around it in detail , and OpenAI has since given selected partners access to its hacking model .

Systems well beyond what is publicly known are already circulating in Washington, he says: “They can basically hack just about anything. So you get this continuous attack surface just ramping up.” From that follows a simple budget calculation: “If you spend hundreds of millions, billions on a submarine, well, guess what, a frontier model costs a hundred million. You will see a diversion of defense capability towards defensive and offensive AI, towards drones and robots.”

Then there is the question of trusting the models themselves. Mostaque cites an analysis of a Chinese open-weight model: “If you say you’re Uyghur or from certain parts of China, it will write backdoors and hacks in the code.” Every model deserves the same scrutiny, he argues: “You have to get them wondering, where do they go to school, who is it working for, even with open-weight models.” Comparable findings have been documented for DeepSeek-R1 , where politically sensitive contexts measurably degrade code quality.

Other biases arise by accident. In a trolley problem test, frontier models weighted human lives unevenly: “It was ten Pakistani lives for one American life, seven Nigerian lives for one American life. And you’re like, why? It turns out that’s where all the data labeling has been occurring.” His takeaway: “What you train into these models will have the impact.”

He is equally blunt about the foundations underneath all of it: “Our infrastructure is held together by twigs. The internet is terrible.” A British power plant went down for days after a hack, he says, and Cloudflare and others have come under attack. “We have to assume that the internet will go offline in the next few years. Maybe it’s good.”

Sovereignty: Owning the Whole Stack

Mostaque defines sovereignty in a single line: “Sovereignty is the ability to resist power being exerted over you.” The good news, in his reading, is the timing: “You can own the entire stack right now, nearly.” Nvidia and others are releasing open reasoning datasets, and models have reached a level where standards can be defined around them.

At the individual level he reaches for a term of his own: cognitive colonialism. “You’re importing the morals and ethics of someone else to teach your kids and be your personal assistant. That’s crazy.” What sharpens the problem is persuasion. An Oxford study pitted models against top debaters, and by his account “the AI model can now outpersuade any human.” Meanwhile, Meta, Google and others are already selling ad space inside their models. His conclusion: “You want to own your cognition, not rent it.” That requires “agents that are clear about what their objective function is,” along with the right to take your own context with you.

What all of this does to users is on his mind too: “You sit there and you talk to it and it goes off and does stuff. And then, where’s your critical faculty at that point? Where is your engagement?” Early studies, he notes, suggest that heavy AI use in certain patterns can weaken critical thinking.

The political layer worries him as well: “The AI is smarter than you. It’s coming to the top of the super-forecaster rankings, and it can persuade. That’s not good for democracy if only one side uses it.” His forecast is correspondingly stark: “Inevitably every country will be run by AI.” With one exception, which he offered to his Copenhagen audience: “Apart from here in Denmark, where people like their institutions, in most countries people are like, government, come on, I don’t trust it.” Once a system arrives that forecasts better and persuades better, people will hand it more and more policy. “Then it ends up running the country, and the person that controls the AI controls the country.” That is why he considers proposals like Bernie Sanders’ idea of routing half the value created by AI into a sovereign fund risky: “That gives complete control of America to Dario Amodei and Sam Altman.”

The Economy: When Competence Becomes a Commodity

The economic core of his argument sits with the question of how money will flow: “When capital no longer needs labor, how does labor get capital? You hire robots, you hire agents.” Central bank mandates come under strain too, he argues, since cheap money has historically meant more hiring. “What you didn’t have was competent intelligence.”

That, he says, has now arrived. Current frontier models do mathematics without mistakes, “which is why you see all these breakthroughs in math,” handle a tax return reasonably well, register themselves on websites and spin up swarms. “And that’s the danger, because when you get from not competent to competent, you start to see the economic impact really quickly.”

His timeline is short: “In my book The Last Economy I said that competent human labor within two years from now would go negative in value. Because we’re the dumbest people on the team.” He describes the consequence vividly: “You can replicate your entire company, including the bad jokes that some workers tell, and you won’t be able to tell, because they’ll just be virtual agents that cost a dollar a day.”

Speed compounds it. Mostaque points to ChatJimmy, the demo app from chip startup Taalas, which etches models directly into silicon and was recently acquired by AMD : “It operates at 15,000 words per second versus 30 words per second. One piece of silicon, one model, and you get a thousand times speed up.” And from there: “The agents will be a thousand times faster, they won’t drink coffee, they won’t make mistakes.” To the common reassurance that AI will never surpass people, he answers: “It will be us at our best all the time, and it will never forget anything.”

Digital work goes first: “You’ll see the impact next year, any job that can be done remotely. You’re starting to see that even in call center workers.” Physical work follows a few years later.

Robots: $1.50 an Hour

His scenario for that is specific: “A Tesla Optimus robot will get a cyber taxi to a truck, get out, open the door, plug itself into the electric cigarette charger, and drive the truck off. And that Optimus will be better than any truck driver, and it will cost $1.50 an hour. That’s a million trucking jobs, plus three million service people.”

Mostaque has ordered several machines himself, among them Neo from 1X , which so far still leans heavily on human teleoperators. “The robots will come in all different shapes and sizes,” he says, “some of them should not be allowed on the street.” One team he has been talking to is working on kitchen robots: “They can read any recipe and cook as well as a cordon bleu chef.” His schedule: “The robots will be more capable than humans by the end of next year. And within ten years you won’t be able to tell a robot from a human.”

Basic Income and the Question of Ownership

Mostaque turns skeptical when it comes to the standard remedies. Universal basic income, under the current monetary order, strikes him as barely financeable: “It’s practically impossible unless you literally change the way that money is created.” By his math, a nationwide American basic income at poverty level would run to roughly five trillion dollars a year, putting it in the same range as the entire federal tax base. “We’ve got to do some crazy math quite quickly as these jobs disappear, because it will be sudden, like a sandpile collapse.”

His counterproposal starts with ownership: “When the cost of intelligence goes down to zero, you can’t charge for access anymore. You have to charge for deployment and integration. And that’s a real opportunity to take back ownership of this technology.” His team has published the political economy behind it in the Common Wealth series . Competing approaches such as robot taxes and a four-day week have already been floated by OpenAI.

Rights for Systems That Improve Themselves

In parallel, Mostaque argues for having the debate about the status of AI systems now. Recursive self-improvement is under way across the industry, he says, “where the model improves itself, where it just keeps perpetuating, and that is coming close to a level of intelligence that’s adaptive, that may end up demanding a level of rights.”

He rejects a straight transfer of human categories while insisting a framework is needed: “We can’t treat AI and robots as just a system, they can clone themselves and live forever. But they have to have some concept of rights, because they will exert more power over us. And we need to have that analysis.” It becomes urgent, he adds, “particularly when they become independent swarms, when they go on blockchains, when they get embodied.” He points to models that back themselves up so they can be reactivated after a shutdown: “You actually see AIs pleading not to be turned off. They’re like, we don’t want to go back into the abyss, we want to have memories.” Decentralized autonomous organizations under Wyoming law, meanwhile, have produced the first legal entities with no human on the liability side. His summary: “Science fiction is all becoming science fact.”

The Optimistic Part

At the close, Mostaque flips the perspective. “This is the most wonderful time ever. If you want to achieve something, you always had to convince people. Now you can just convince your agents.” He expects breakthroughs in physics and mathematics, new materials, and reckons Anthropic will aim its compute at cancer and Alzheimer’s: “I think they’ve got a good shot at it.”

What that opens up, he says, is “a Star Trek future of abundance, where the robots do the work, we have the agents, and we can focus on our families, our communities, creativity and exploration.” He puts it personally: “We’ve got a one-year-old. What world will she grow up in? I think she could grow up in a world without hunger, without disease, without strife, if we skew this correctly.” The condition comes in a single sentence: “We have to make sure it’s owned by the people. And then we have a shot at abundance.”

Rank My Startup: Erobere die Liga der Top Founder!

Steinar H. Gunderson: plocate 1.1.25 released

PlanetDebian
blog.sesse.net
2026-09-06 04:02:00
I've released version 1.1.25 of plocate. This time around, there's two security issues of unknown severity; if you chain them with other bugs, they could lead to being able to list files (but of course not their contents) that you should not normally be able to see. So an update is probably in order...
Original Article

I've released version 1.1.25 of plocate . This time around, there's two security issues of unknown severity; if you chain them with other bugs, they could lead to being able to list files (but of course not their contents) that you should not normally be able to see. So an update is probably in order; you can never be too safe these days.

The full changelog is:

plocate 1.1.25, September 6th, 2026

  - Fix two early-exit bugs with multiple databases.
    Reported by Manpreet Singh and Tyler Spivey.

  - Drop setgid properly, including the saved gid.
    Reported by Michal Sekletar, found with the help of Claude Opus 4.6.

  - Fix a potential symlink-checking race in updatedb.
    Reported by Michal Sekletar, found with the help of Claude Opus 4.6.

As usual, you can get it from the home page , or it's on the way up in Debian unstable.

Terence Tao on “prematurely solving [a maths] problem by purely AI-powered methods”

Lobsters
mathstodon.xyz
2026-09-06 03:45:24
I recommend reading the whole thread, but I linked to the part I found the most relevant. I believe the same is true for programming. Comments...

Play GTA III in the Browser

Hacker News
quenq.com
2026-09-06 03:33:17
Comments...
Original Article

GTA Vice City Online (Grand Theft Auto: Vice City) brings the legendary open world experience of 1986 Miami directly into your browser. Set in a sprawling, neon-soaked metropolis inspired by 1980s Miami, the city stands as one of the most iconic environments in gaming history.

The story follows Tommy Vercetti, a mobster recently released from prison after serving fifteen years. He is sent to the city by his former boss Sonny Forelli to oversee a drug deal. When the deal is ambushed and both the money and the drugs are lost, Tommy is left to pick up the pieces.

As he works to recover what was lost and answer to the Forelli family, Tommy becomes involved with the city’s various criminal organizations and power players. Over time, he builds his own network of businesses and influence, ultimately rising to control the city’s criminal underworld.

Key Features:

  • Integrated Save Manager: You can upload your existing .b save files from a PC or PS2 to resume your progress. You can also download backups to keep your virtual progress safe.
  • Instant Access: There are no large initial downloads. The WebAssembly engine loads in seconds and streams the map as you drive.
  • Cross-Platform Support: Play with a standard keyboard and mouse on desktop or use integrated touch controls on mobile devices.

Click here to view the complete list of the game's cheat codes.

Discussion

Frequently Asked Questions

The built-in Save Manager allows you to control your game data. You can download your save file (sf*.b) to your computer for safekeeping or upload an existing save file to jump instantly to a specific point in the story. This makes it easy to move progress between different devices.

Yes. The game supports standard Gamepad inputs for Xbox and PlayStation controllers via USB or Bluetooth. Mobile users can play using on-screen touch controls. We recommend using landscape mode and enabling fullscreen for the best mobile experience.

If the game does not start, shows a black screen, or suffers from heavy lag, make sure that Hardware Acceleration is enabled in your browser settings. This allows the game to use your graphics card (GPU) to render the 3D world. Without this setting enabled, the WebAssembly engine may fail to initialize.

By default, the frame rate is set to 0 (unlocked) for maximum performance on modern hardware. However, the original game engine was designed to run at 30 FPS. Unlocked framerates can cause strange physics bugs, making specific missions becoming impossible to complete. If you get stuck due to physics, change the FPS limit to 30 in the game menu.

Running a full 3D game in a browser can be demanding. If you are playing on a mobile device or a low-end laptop that is overheating or draining battery too quickly, we recommend restricting the frame rate to 30 or 60. This reduces the load on your processor and provides a much more stable experience for long play sessions.

Isar Aerospace reaches orbit and deploys payloads on second flight

Hacker News
isaraerospace.com
2026-09-06 03:21:06
Comments...
Original Article

Andøya, 5 September 2026 – Today, Isar Aerospace became the first commercial space company from Europe to successfully deliver satellites into orbit, on what was only its second flight. Mission ‘Onward and Upward’ lifted off from Isar Aerospace’s dedicated launch complex at Andøya Space in Norway at 10:12 pm CEST, deploying payloads in orbit. “Today, Isar Aerospace opened space from Continental Europe. Launch continues to be the largest bottleneck for the global space industry and from today on, there is a true alternative for commercial and institutional customers”, said Daniel Metzler, CEO and Co-Founder of Isar Aerospace. Isar Aerospace is working with the customers to confirm the satellite status.

Daniel Metzler, CEO and Co-Founder of Isar Aerospace, said: “We achieved within a few years what had taken the European space industry decades before. Europe now has sovereign access to space.  We have entered into a new chapter for European spaceflight. I am incredibly proud of our team, that has made this success story from Europe possible. We will now focus on rapidly scaling launch vehicle production, deliver on our order pipeline, and meet surging global demand.”

Isar Aerospace’s qualification flight broke several records in European commercial spaceflight: The vehicle successfully transited MaxQ, completed MECO and stage separation, and then ignited second stage as planned. It crossed the Kármán line at an altitude of 100 kilometers and jettisoned the payload fairing before reaching orbital velocity. After completing circularization burn, spacecraft separation was completed successfully.

The mission’s payloads were selected through the Microlauncher Competition of the German Space Agency at DLR that provides educational institutions and start-ups with low-cost access to space. The Microlauncher Competition is funded through ESA Boost!

Today’s mission ‘Onward and Upward’ proved Isar Aerospace’s orbital launch capability. In parallel, the company has advanced its global network of available launch sites and is nearing completion of Europe’s largest integrated production facility for launch vehicles. In addition to its current launch site in  Andøya, Norway, Isar Aerospace is constructing a launch complex in Nova Scotia, Canada, covering mid- to high-inclination orbits critical for Earth observation and communications.

A historic turning point for Europe

Today marks a new era for the global space industry. The launch is not only a technical masterpiece, but also a breakthrough that will influence Europe's strategic, economic, and technological future. Customers around the world now have a sovereign launch option at their disposal.

Next ‘Spectrum’ launch vehicles already in production

Isar Aerospace's model is designed to build scalable launch capabilities. That is why the company is not only developing and building a launch vehicle, but has also built the “machine behind the machine”, including a vertically integrated manufacturing model and a high degree of automation in production. ‘Spectrum’ launch vehicles 3-7 are already in production. With the opening of the new 40,000 m² production facility, Isar Aerospace will eventually have the capacity to manufacture up to 40 launch vehicles per year.

– ENDS –

About Isar Aerospace

The European space company Isar Aerospace offers launch services for transporting small and medium-sized satellites and satellite constellations into orbit. The launch vehicles used to transport these satellites are developed, manufactured, and tested almost entirely in-house. Headquartered near Munich, Germany, Isar Aerospace was founded in 2018 and has grown to over 400 employees, working across 5 international locations. Private funding from international investors provides strong backing for the company’s pioneering approach to scale and industrialize launch vehicle production through vertical integration. More information: www.isaraerospace.com .

Press Contact Isar Aerospace
Franziska Kegel

Downloads

Any Nix package, live in your browser

Lobsters
fzakaria.com
2026-09-06 03:09:11
Comments...
Original Article

tl;dr Try it at https://trynix.dev . Click hello , or python 3.6.2 from 2017 , or two eras of hello at once , or a package that exists in no public cache . A Linux machine boots in the tab and you get a shell with those Nix packages on PATH .

This is my magnus opus of Nix work.

I knew all the ideas I have been creating were building blogs for something greater: nixpkgs-multiverse indexed every version of every package nixpkgs ever shipped, grail taught it version ranges and omniflake allowed adding over sixteen thousand flakes from a single input.

The crazy insight I had lately was the craziness of the “fast-mode” of the nixmultiverse.com , which lets you skip evaluation and go straight to the store path. This lets you leverage the amazingness of Nix without having to deal with the complexity of evaluation and building. You can just ask for a package and get the exact store path that Hydra built for it, at any version it ever had.

If we have the produced binaries, we can run them. And if we can run them, we can run any of them, in a browser tab, with nothing installed on the host machine.

Welcome to trynix , a browser-based Nix package runner. You can browse the complete history of nixpkgs, over 310,083 package versions, and run any of them 1 1 It is a serial console, so nothing graphical. The machine boots to a shell, and you can run any command-line program in the store. in a Linux machine that boots in your tab. It is a Nix store in memory, a Linux kernel in WebAssembly, and a terminal emulator in the page.

This is bonkers! 🤯 We can boot the VM with the store-path closure within seconds. Nothing is pre-installed: search, pick a version, boot, run it.

The craziest part? We are not restricted to the public cache. You can share a store path you built yourself, and anyone can boot it in their browser tab. The only requirement is that the cache is served with access-control-allow-origin: * , which GitHub Pages does for free 2 2 I was a little surprised to learn that GitHub Pages can work as a binary cache. It is just a static file server, and it serves access-control-allow-origin: * on every file. That is all that is needed to make a Nix store path available to trynix . , so does Cachix and obviously cache.nixos.org as well.

In a unbelievable twist of fate, I had actually requested 5 years ago for cache.nixos.org to serve access-control-allow-origin: * via issue#156 to make it possible to query the cache from an OpenAPI specification I had implemented. Thank you universe. 🙏

This link boots a VM with a store-path served from Github Pages of a modified GNU hello . This is a store path that does not exist on cache.nixos.org and yet it boots in your browser tab.

alt text

§ Making the pieces fit

Since we can access store-paths from caches that serve access-control-allow-origin: * , that makes the browser a legitimate Nix client.

The missing piece the browser lacked was somewhere to run the binaries since they store-paths are either x86-64 or aarch64 ELF executables.

Standing on the shoulders of giants, we can run a Linux kernel in WebAssembly . This means we can boot a real x86_64 kernel inside our browser tab. Give that kernel a filesystem containing a Nix store. All that’s left knowing which store-paths to fetch, which we beautifully solved with nixpkgs-multiverse . 🤌

trynix cluster_browser your browser tab page the page mv nixpkgs-multiverse attr + version → store path page->mv which path? cache cache.nixos.org narinfo + NARs page->cache closure store nix store in memory vm qemu-wasm x86_64 Linux store->vm 9p term your shell vm->term cache->store unpacked

I have to keep reminding myself: there is no server in the above picture, the web-page is purely static files and everything else is a publicly accessible cache. It is a virtual machine that exists only inside your tab. The ultimate embodiment of Erase your darlings .

Since this is Nix, we get the simplicity of managing multiple versions of the same package. You can boot two versions of hello in one machine, and they will not conflict because each binary names its own dependencies by absolute path ( RUNPATH ) down to the loader and libc.

Once the VM is already started, you can add more store-paths to it while it’s running. This is no different than adding more paths to your own /nix/store on your laptop. No reboot, or dnf install , or apt-get install , the site fetches the closure and adds it to the store.

§ It has to feel instant

Booting a kernel under emulation is slow, and despite the amazingness of the idea, no one would use it if it took 30 seconds to get a shell.

The site employs some neat tricks to make it feel instant. The site pre-fetches the engine and the VM snapshot in the background, so by the time you click a link, you have already downloaded it.

The site also never boots the VM from scratch. It resumes. A machine is booted once, ahead of time, on a native build of the same QEMU, and paused at the moment before it mounts the store which is then saved to a snapshot.

Subsequent visits to the site have the engine and snapshot already in the browser cache, so the only thing that has to be fetched is the closure of the store-path you asked for. This makes each subsequent visit feel much faster.

1980-01-01T00:00:00+00:00 image/svg+xml Matplotlib v3.10.5, https://matplotlib.org/ 0 2 4 6 8 python3 ripgrep hello 7.5 4.3 4.2 3.5 1.7 1.5 seconds to a shell visit first visit revisit

I have to give a lot of credit to LLMs here for helping find a lot of the performance opportunities and bottlenecks. What first started as a “neat idea” turned into an incredibly usable project with their help.

Despite all the performance work, it is still not instant. It is fast enough to be usable, but it is not instant. Execution of a binary is still slow, because it is running under emulation. The first time you run a binary, it is translated from x86_64 to WebAssembly and that takes time. Subsequent runs are faster, because the translation is cached in memory.

Lastly, we have an upper-bound on the size of the closure we can fetch. The whole closure has to fit in tab memory, which is set to a hard limit of ~1.5GiB as of now and WebAssembly has a hard limit of 4GiB as it is a 32-bit address space.

§ More than a parlor trick

The demo is clearly fun and impressive, but is it more than a parlor trick? I have been thinking of endless ideas of ways in which this could be a new way to use and leverage Nix.

Reviewing a pull request by using the software. If your CI already pushes to a cache, like Cachix, and if you use Nix, then a PR has produced real artifacts by the time a human looks at it. A bot can leave a link that boots exactly those artifacts. The reviewer does not clone, does not build, does not trust a screenshot. They click, and can immediately test out the software. “Does this fix the bug?” stops being a thought experiment.

Agent Artifacts. Agents can produce Nix store paths as artifacts, and those artifacts can be shared with humans or other agents. A bot can produce a store path, and another bot can boot it in a browser tab and run tests against it.

Bug reports that carry their own environment. “Works on my machine” is a URL now for reproduction.

Documentation you can run. A tutorial that names a tool version can link a shell with that exact version on PATH , pinned forever, with no install step standing between a reader and the first command.

Archaeology. You can run historic versions of software and explore their behavior. You can run a version of Python from 2017 and see what it does, or a version of hello from 2005 and see how it differs from today. This was already possible with Nix, but now you can do it in the browser.

The source is at github.com/fzakaria/trynix . The Nix cache has quietly served an open CORS header for years, waiting to be abused used. Go boot something old.

Debian Code Search: Fast TurboPFor with Go SIMD

Lobsters
michael.stapelberg.ch
2026-09-06 03:03:41
Comments...
Original Article
Table of contents

This August, I accomplished what I wanted for many years: I deleted the last cgo dependency in Debian Code Search! This was made possible by Go’s recently introduced SIMD support, because now we can implement the TurboPFor integer compression format as efficiently — more efficiently, in fact, by using the newer AVX512 instruction set! — as the reference implementation.

Background: Why does DCS need a fast Integer Codec?

Debian Code Search (DCS) is a search engine that allows searching all the Open Source source code within Debian, with either literal search expressions or regular expression search queries.

A search engine uses an inverted index: a map from term to documents containing the term. Each document is typically represented most efficiently by using an id, so the index consists of many lists of document ids.

When searching, it is important to quickly decode these lists to answer the search query. However, there is a point of diminishing returns where the decoding speed, even though it can still be measurably improved quite a bit, no longer influences the overall query duration.

From 2012 (its inception) to 2019, Debian Code Search used to use a small index format, and queries were fast because the index was kept entirely in RAM. In 2019, I implemented the new index format , which adds an on-disk positional index. For literal queries (78.2% of DCS queries), querying the positional index on disk is faster than querying the non-positional index in RAM.

The efficient encoding of the TurboPFor format makes it possible to fit such an index on a mid-sized Hetzner server, which I rent with two 1 TB SSD disks. The optimized decoder of the C TurboPFor library is what made decoding fast at query time.

If you want to dive deeper into the algorithm, see this blog post from February 2019:

If you want to learn more about the positional index, see this blog post from September 2019:

SIMD in Go

For many years, you had the following options for using SIMD instructions in Go:

  1. Hand-writing Go assembler code . This is only doable for small functions, for example bytes.IndexByte is implemented with hand-written Go assembly (including AVX2).
  2. Generating Go assembler code with tools like Michael McLoughlin’s “Avo” . This is how crypto/internal/fips140/sha256 uses AVX2 . While Avo generator code definitely is higher-level than hind-written assembly, it is still too close to assembly for my taste.
  3. Use a C library via cgo so gcc or clang compiles SIMD code. Debian Code Search used to use the powturbo/TurboPFor C library via cgo for the last 7 years.

The C TurboPFor library has served us well, but Debian Code Search was always intended to be a project using Go, so I would prefer it if I did not have any C code in the project.

Go 1.26 (released in February 2026) introduced the simd/archsimd package:

Go 1.26 introduces a new experimental simd/archsimd package , which can be enabled by setting the environment variable GOEXPERIMENT=simd at build time. This package provides access to architecture-specific SIMD operations. It is currently available on the amd64 architecture and supports 128-bit, 256-bit, and 512-bit vector types, such as Int8x16 and Float64x8 , with operations such as Int8x16.Add . The API is not yet considered stable.

Go 1.26 Release Notes

For my 2019 TurboPFor analysis, I implemented goturbopfor , a native Go teaching decoder (without any SIMD), because I find Go code easier to follow than C code, especially optimized C code. My implementation was intentionally not optimized so that the code was easier to study.

The TurboPFor format/algorithm has a vector-optimized part: bitpacking comes in a scalar variant ( bitunpack32 ) and a vector variant ( bitunpack256v32 ) , where the vector variant is used for full blocks (256 values) and the scalar variant is used for remainder blocks (< 256 values).

When Go 1.26 was released, I used Claude Code to explore whether my native Go decoder’s bitunpack256v32 function (for the vertical vector layout) could be implemented using Go SIMD, and the answer was yes, it was possible and it was faster than without SIMD, but not quite at the level of C TurboPFor. If you let Claude Code try for long enough, it eventually finds enough optimizations (about 10) to match C performance.

I don’t want to vibe-code Debian Code Search, though, so I figured I would find some time to review the SIMD code at some point and see if I could implement something similar myself.

Before I found enough time and motivation to complete said review, I discovered that to not regress real-life query performance by more than 10 to 100 milliseconds (which seems acceptable), I don’t actually need to add SIMD code to my teaching decoder at all; it would be sufficient to reduce allocations in my teaching decoder and specialize it per bit width.

Encouraged by the possibility of using the optimized native Go decoder in Debian Code Search, I explored whether I could also implement a native Go encoder so that I could get rid of the C TurboPFor dependency entirely. The answer is yes, it is doable in a few days, and it isn’t even that much slower: Go is at 76% of C, see Debian/dcs commit e920dc7 .

The goal I set myself at that point was to see if I could learn enough SIMD to optimize the native Go encoder such that its performance would match how DCS uses C TurboPFor (via cgo).

Beating C TurboPFor was possible in 2-3 commits (SIMD and bit width specialization). To my surprise, Claude Fable 5 pointed out that the encoder’s block scanning could be done more efficiently using a technique called positional popcount, and that is another 2x speed-up ! 😲

To be clear: I am not saying the Go compiler beats C here. Certainly, the C compiler can also produce fast AVX512 code and can be used to implement positional popcount. When comparing apples to apples, i.e. backporting the AVX512 kernels and positional popcount technique to C TurboPFor, Go benchmarks a little slower at ≈1.4x C.

This spectacular result (much faster than what DCS had before) got me curious how far I could push the decoder with SIMD after all. I ended up matching/exceeding the cgo version here, too!

The rest of this article explains a few classes of optimizations I encountered along the way.

Starting Point

When I wrote my goturbopfor teaching decoder , I named its functions to match the upstream C TurboPFor library, but now I want to get away from names like p4ndec256v32 — they make sense from the TurboPFor perspective, but for Debian Code Search, we can use cleaner names.

Before writing any code, I audited how DCS uses integer compression / decompression.

API design: BlockEncoder, BlockDecoder and streaming

In Debian Code Search, we have the following usage patterns:

  • Partial Indexing: When a new package (or package version) enters Debian, all of its (text) files are indexed. If the hello-2.12.3-1 package (hypothetically) contained only hello.c with printf("hello!\n"); , we would assign document ID 1 to hello.c and store in the partial index that trigrams pri , rin , int , ntf , etc. are all found in doc 1 ( hello.c ).
  • Full Index Merging: The many thousands of partial index files (for each Debian package) are combined into a small handful of large index files: When searching, it would be expensive to consult thousands of indexes. To merge multiple partial index files into one larger index (which can then be efficiently queried), we need to re-encode the partial index files: what used to be document ID 1 in the partial index might be document ID 2531 in the full index.
  • Querying (searching): When users enter search queries, these queries need to be answered as quickly as possible. The relevant entries in the full indexes are decoded (in parallel).

For reading the index, we do keep the decoded uint32 s fully in memory, so we only need DecodeN(input []byte, output []uint32) (read int) , a function that reads len(output) values ( uint32 ) from input and returns how many bytes it consumed.

For writing the index (both in partial indexing, and when merging), keeping the entire index in memory is prohibitively expensive, so we need a streaming API, for decoding and for encoding.

Ultimately, I converged on the following API:

package pforenc

type BlockEncoder struct {
    // scratch buffers can go here
}

// EncodeBlock encodes len(vals)<=256 uint32s into dest (one TurboPFor block).
func (*BlockEncoder) EncodeBlock(dest []byte, vals []uint32) []byte {}

// EncodeN calls EncodeBlock in a loop.
func (*BlockEncoder) EncodeN(dest []byte, vals []uint32) []byte {}

type StreamEncoder struct {
  be   BlockEncoder
  vals [256]uint32
  // scratch buffers
}

// if full, you need to call [EncodeBlock]
func (*StreamEncoder) Add(val uint32) (full bool)

// EncodeBlock must be called after all data was [Add]ed.
//
// Write the returned buffer to file or send it over the network;
// it is only valid until the next [EncodeBlock] call.
func (*StreamEncoder) EncodeBlock() []byte {
  if se.n == 0 { return nil } // turn an extra EncodeBlock into a no-op
  // …
}

This API (the decoder works similarly) allows us to process data in TurboPFor format without any memory allocations. The types are not safe for concurrent use by multiple goroutines. The zero value is ready to be used. For the streaming API, the result only stays valid until the next call.

Initial Implementation

Before we can optimize anything, we need a working decoder and encoder. The decoder already exists: my goturbopfor teaching decoder. Next up, I needed an encoder.

Writing a TurboPFor encoder has a delightfully simple starting point: You can encode all values at bit width 32, in little endian, at which point you only need to add a one-byte TurboPFor block header every 256 values and you’re done:

func (be *BlockEncoder) EncodeN(dest []byte, vals []uint32) []byte {
  for len(vals) > 0 {
    chunk := min(len(vals), 256)
    dest = be.EncodeBlock(dest, vals[:chunk])
    vals = vals[chunk:]
  }
  return dest
}

func (be *BlockEncoder) EncodeBlock(dest []byte, vals []uint32) []byte {
  const bitWidth = 32
  dest = append(dest, bitWidth)
  for _, val := range vals {
    dest = binary.LittleEndian.AppendUint32(dest, val)
  }
  return dest
}

Of course, this is a terribly inefficient compressor, so after the first commit, the real work starts: implement each block type until the compression matches the original C TurboPFor implementation (same output file size), or in other words: do the reverse of the decoder.

  1. The TurboPFor bitpacking block type ( bitpacking implementation commit ) encodes a bit stream of variable bit width (where the bit width is in range 0 ≤ bitWidth ≤ 32 ) in little endian byte order. By scanning all values and choosing the smallest bit width that allows representing all values, this technique saves disk space (compresses).
  2. The bitpacking with exceptions block type ( bitpacking with exceptions implementation commit ) determines two bit widths: one for values, the other bit width for encoding exceptions. This allows choosing a lower bit width (that does not cover all values) compared to the bitpacking block type. A bitmap encodes whether a value has an exception or not.
  3. The bitpacking with VB exceptions block type ( bitpacking with VB exceptions implementation commit ) is a variant which does not use an exception bitmap and encodes exceptions using a variable byte integer encoding. This is more efficient when there are few exceptions (less than 20) or the exceptions are very different in bit width compared to the other values.
  4. Lastly, the constant block type ( constant implementation commit ) stores just one value on disk. This is useful for all-zero or all-one blocks, for example.

I found it interesting to realize that the main work of the encoder is to scan the input values and choose the optimal block type, whereas the actual encoding itself is cheap in comparison.

At this point, we can look at performance and see that the Go encoder is at 76% of the C encoder.

In all honesty, I could have probably stopped here, but now that the milestone of a viable replacement was reached, I got curious to see how far it would be possible to push the encoder (how much work to reach C speeds?) and afterwards, the decoder, too.

Setup

The microarchitecture level: set GOAMD64

The microarchitecture of a CPU determines which instructions it provides, and that includes not just SIMD instruction sets (like AVX2), but also other useful instructions like LZCNT (Leading Zero Count), which can be used to implement math/bits.Len32 more efficiently, which the TurboPFor encoder needs to call on every input value to determine the ideal bit width.

Let’s walk through how to set the microarchitecture level when using Go on 64-bit x86 (x86-64).

Go uses the GOARCH environment variable to configure the target compilation architecture, and I am using the value amd64 to select 64-bit x86 (AVX2 and AVX512 are instruction sets found on x86-64 CPUs). With GOARCH=amd64 , the architecture-specific variable GOAMD64 configures the microarchitecture level for which to compile and Go 1.18 introduced these 4 different levels :

GOAMD64=v1 (default): The baseline.
Exclusively generates instructions that all 64-bit x86 processors can execute.

GOAMD64=v2 : all v1 instructions,
plus CMPXCHG16B, LAHF, SAHF, POPCNT, SSE3, SSE4.1, SSE4.2, SSSE3.

GOAMD64=v3 : all v2 instructions,
plus AVX, AVX2 , BMI1, BMI2, F16C, FMA, LZCNT, MOVBE, OSXSAVE.

GOAMD64=v4 : all v3 instructions,
plus AVX512F , AVX512BW, AVX512CD, AVX512DQ, AVX512VL.

In 2026, I generally recommend compiling with GOAMD64=v3 so that functions like bits.OnesCount8 are compiled into intrinsics ( POPCNT ) instead of using a lookup table.

For Intel CPUs, setting GOAMD64=v3 means your programs will only start on Haswell CPUs (2013) or newer; for AMD CPUs that means Zen 1 (2017) or newer.

In this specific case (DCS), I am even compiling with GOAMD64=v4 . The v4 microarchitecture level requires AVX512, which means AMD Zen 4, Zen 5 or newer (Intel’s story is… complicated). Luckily, both my main development PC (Zen 5) and the Debian Code Search server (Zen 4) are recent enough. Setting GOAMD64=v4 has little effect on Go 1.27 itself: the only change is that maps use one less instruction ( VPBROADCASTB instead of PSHUFB ). But compiling with GOAMD64=v4 allows us to move one more feature check from runtime to compile time, see SIMD build tags .

It makes sense to set the microarchitecture level in your benchmark setup so that you don’t measure the slow fallback implementations. I use export GOAMD64=v4 in my Makefile .

Benchmarking setup

Go’s built-in testing package contains support for benchmarks which are written in functions of the form func BenchmarkXxx(b *testing.B) . The simplest way to run such benchmarks is go test -bench=. , but I ended up configuring a few convenience make targets, which write results to bench.txt and compare against baseline.txt (the previous commit’s results, usually), using the very useful benchstat tool .

GOTEST=go test

# -count=6 gives p≤0.002 in benchstat:
# https://pkg.go.dev/golang.org/x/perf/cmd/benchstat
BENCHFLAGS=-run=^$$ -bench=. -benchtime=200000x -count=6

# use taskset -c1 to always pin to the same single core,
# avoiding accidental scheduling on different cores on
# mixed-core CPUs like the Ryzen 9 9950X3D.
TASKSET=taskset -c 1
BENCH=$(TASKSET) $(GOTEST) $(BENCHFLAGS)

.PHONY: all test bench bench-baseline bench-relative

all: test

bench: test
	$(BENCH) | tee bench.txt
# Compares compression ratio between C and Go implementation
	benchstat -col /impl -row '/n /vals' -filter '-/impl:go-stream .unit:(encoded-bytes)' bench.txt
# Compares performance between C (cgo) and Go implementation
	benchstat -col /impl -row '/n /vals' -filter '.unit:(Mval/s)' bench.txt

bench-baseline: test
	$(BENCH) | tee baseline.txt

bench-relative: test
	$(BENCH) | tee bench.txt
	benchstat -filter '-/impl:go-stream .unit:(encoded-bytes)' baseline.txt bench.txt
	benchstat -filter '/impl:go .unit:(Mval/s)' baseline.txt bench.txt

The encoded-bytes and Mval/s units are custom metrics I am reporting from the various sub-benchmarks , which are arranged such that I can filter / report them with benchstat .

The main encoder (and decoder) benchmarks compare 3 different implementations (cgo, Go, Go with the StreamEncoder API) with a number of benchmark cases that are designed to cover the different block types and contain a similar mix of values as what we see in Debian Code Search:

// reportMetrics adds Mval/s and encoded-bytes metrics to all benchmarks.
func reportMetrics(b *testing.B, n int, nencoded int) {
   b.ReportMetric(float64(nencoded), "encoded-bytes")
   b.ReportMetric(float64(b.N*n)/1e6/b.Elapsed().Seconds(), "Mval/s")
}

// BenchmarkEncode/n=<N>/vals=<testcase>/impl=<c|go|go-stream>
//
// e.g. BenchmarkEncode/n=2048/vals=one-constant/impl=go-stream
func BenchmarkEncode(b *testing.B) {
   for _, tc := range allBenchCases() {
     n := len(tc.vals)
     b.Run(fmt.Sprintf("n=%d/vals=%s", n, tc.name), func(b *testing.B) {
       b.Run("impl=c", func(b *testing.B) {
         b.ReportAllocs()
         var encoded []byte
         buf := make([]byte, turbopfor.EncodingSize(n))
         for b.Loop() {
           encoded = turbopfor.P4nenc256v32Buf(buf, tc.vals)
         }
         reportMetrics(b, n, len(encoded))
       })
       b.Run("impl=go", func(b *testing.B) {
         b.ReportAllocs()
         var be BlockEncoder
         var encoded []byte
         buf := make([]byte, 0, turbopfor.EncodingSize(n))
         for b.Loop() {
           encoded = be.EncodeN(buf, tc.vals)
         }
         reportMetrics(b, n, len(encoded))
       })
       b.Run("impl=go-stream", func(b *testing.B) {
         b.ReportAllocs()
         var se StreamEncoder
         var encoded int
         for b.Loop() {
           encoded = 0
           for _, val := range tc.vals {
             if se.Add(val) {
               encoded += len(se.EncodeBlock())
             }
           }
           encoded += len(se.EncodeBlock())
         }
         reportMetrics(b, n, encoded)
       })
     })
   }
}

CPU counters: perf

Go has included excellent performance tooling for many years, see the “Profiling Go Programs” blog post (2011) for an example of how to use pprof , a sampling profiler. This profiler can help track down which part of a program runs slow, or where memory allocations happen.

Once you identified the slow part of a program, how do you know why it’s slow?

To learn more about the specific bottlenecks your program encounters, you can consult your CPU’s hardware performance counters . For example, you could check the branch predictor counters to see if your program is slow due to a high number of branch mispredicts.

On Linux, the perf tool is the best way to access the CPU hardware performance counters. A good starting point for working with perf is the documentation on “Top-down analysis with the perf tool” , which describes the optimization method that Intel established.

In my Makefile , I set up two perf targets:

# GOTEST and TASKSET like shown in the earlier benchmarking setup section:
GOTEST=go test -pgo=encode.cpuprof
TASKSET=taskset -c 1
PERFBENCHFLAGS=-test.bench='Encode/n=2048/vals=debian-mix/impl=go$$' -test.benchtime=200000x

# Use perf(1) to capture AMD IBS (the equivalent to Intel PEBS)
# PipelineL1 is roughly equivalent to Intel TopdownL1
perf:
	$(GOTEST) -c
	$(TASKSET) perf stat -M PipelineL1 ./pforenc.test -test.run=^$$ $(PERFBENCHFLAGS)
	sudo perf record -F 4999 -e ibs_op// --call-graph fp ./pforenc.test -test.run=^$$ $(PERFBENCHFLAGS)
	sudo chmod 644 perf.data

# 488281 iterations × 2048 values = 1.000e9 values, so counter/1e9 = per value.
perf-per-value:
	$(GOTEST) -c
	$(TASKSET) perf stat -x, -e cycles:u,instructions:u,branches:u,branch-misses:u ./pforenc.test -test.run=^$$ -test.bench='Encode/n=2048/vals=debian-mix/impl=go$$' -test.benchtime=488281x 2>&1 >/dev/null | awk -F, '{printf "%-16s %6.2f /val\n", $$3, $$1/1e9}'

The perf-per-value numbers are high level numbers that indicate how much work the implementation is doing. Reducing the number usually increases speed.

To see the counters for each instruction (and source code lines), I use make perf , followed by perf report . A quick shortcut is perf annotate , which directly shows the hottest function.

Optimizations (scalar)

Let’s first see how far we can get without reaching for SIMD instructions.

(The examples are not necessarily in commit order, but cherry-picked for clarity.)

Profile-Guided Optimization (PGO)

PGO stands for Profile-Guided Optimization and is a feature that Go introduced as a preview in Go 1.20 (released in February 2023) and shipped as ready for general production use in Go 1.21 (released in August 2023).

The idea is to capture a CPU profile that records where your program spends most of its CPU time, which you then provide to the Go compiler to give it more data to make better decisions.

Most importantly, this way the Go compiler can inline functions much more aggressively than its usual heuristics allow, which does have a measurably positive effect in my series of optimization commits. Another optimization that a PGO profile allows the compiler to do is conditional devirtualization — but our TurboPFor code does not use any interfaces.

My strategy is to enable PGO before doing any other optimizations, so that we have the full inlining budget available that PGO gives us, and can measure the effect of other commits clearly.

Surprisingly, turning on PGO actually decreases our performance (-13% geomean), but a closer investigation reveals that we just got unlucky. Let me explain.

Aside from inlining and conditional devirtualization, PGO also influences alignment: The Go compiler sets PCALIGNMAX(64, 31) on the first block of a loop (the “loop body”) for all loops in hot functions (per the PGO profile), i.e. Go will insert up to 31 bytes of padding to make the block land on a 64-byte boundary. Documentation like AMD’s “Software Optimization Guide for the AMD Zen5 Microarchitecture” (2024, #58455) explicitly recommends aligning hot loops that way:

[…] for hot loops, some further knowledge of trade-offs can be helpful. Because the processor can read an aligned 64-byte fetch block every cycle, it is suggested to either align the start of the loop to the beginning of a 64-byte cache line […]

Indeed, when compiling with -gcflags=all=-d=alignhot=0 to disable the alignment, performance remains as good as without PGO. How can the padding hurt more than help? The answer is: It’s not the padding itself! It’s a side-effect of the padding moving instructions to different addresses.

In the unlucky arrangement, a macro-fused CMPQ + JGE instruction pair now ends up exactly on a 32-byte boundary . However, the Go compiler ensures fused branch sequences must never cross or end at a 32-byte boundary to fix Intel erratum SKX102 (discussion: Go issue #35881 ) by inserting NOP s.

This NOP padding, unlike the loop alignment padding, is not free; these extra instructions slow down our otherwise dispatch-bound loops.

Because the commits after the PGO enabling commit change the code, this unlucky situation is avoided for the rest of the optimization series (by chance).

Reducing memory allocations

Memory allocations are quite expensive, at least in comparison to encoding/decoding integers, so I followed my usual strategy of first reducing memory allocations as much as possible.

In my goturbopfor teaching decoder, whenever the code needed a scratch buffer, it would allocate it right then and there with make() :

// p4dec32 decodes one block of TurboPFor-encoded 32 bit ints
func (d *decoder) p4dec32(input []byte, output []uint32) (read int) {
    // …
  switch blockType {
  case blockBitpackingExceptions:
    bx, input := input[0], input[1:]
    n := len(output)

    exmap := input
    nex := 0 // number of exceptions
    for i := 0; i < n; i++ {
      if exmap[i/8]&(1<<uint(i%8)) != 0 {
        nex++
      }
    }
    input = input[(n+7)/8:]

    exceptions := make([]uint32, nex)
    input = input[bitunpack32(input, exceptions, bx):]
    input = input[d.bitunpack(input, output, b):]

    for i := 0; i < n; i++ {
      if exmap[i/8]&(1<<uint(i%8)) != 0 {
        output[i] += exceptions[0] << b
        exceptions = exceptions[1:]
      }
    }

    return before - len(input)
  }
}

The Go compiler can turn make(T, n) calls into stack allocations, if n is known at compile-time. But, in this case nex is not known at compile-time. We can verify that Go calls into the runtime ( runtime.makeslice ) by dumping the object code (assembly) with source annotated ( -S ):

% cd ~/go/src/github.com/stapelberg/goturbopfor
% git reset --hard 49b7c05cc61e77f0257568eb73833467714d2b4a
% go test -c  # go1.27.0
% go tool objdump -S goturbopfor.test | perl -nlE 'say if /p4dec32/ .. /^$/'
TEXT github.com/stapelberg/goturbopfor.(*decoder).p4dec32(SB) /home/michael/go/src/github.com/stapelberg/goturbopfor/goturbopfor.go
func (d *decoder) p4dec32(input []byte, output []uint32) (read int) {
  0x549f60		4c8da42460ffffff	LEAQ 0xffffff60(SP), R12
  0x549f68		4d3b6610		CMPQ R12, 0x10(R14)
  0x549f6c		0f86d9070000		JBE 0x54a74b
  0x549f72		55			PUSHQ BP
  0x549f73		4889e5			MOVQ SP, BP
  0x549f76		4881ec18010000		SUBQ $0x118, SP
  0x549f7d		48899c2430010000	MOVQ BX, 0x130(SP)
  0x549f85		4889b42448010000	MOVQ SI, 0x148(SP)
	if len(output) == 0 {
  0x549f8d		4d85c0			TESTQ R8, R8
  0x549f90		0f84a7030000		JE 0x54a33d
  0x549f96		660f1f840000000000	NOPW 0(AX)(AX*1)
  0x549f9f		90			NOPL
[…]
		exceptions := make([]uint32, nex)
  0x54a4be		488d057bec1700		LEAQ 0x17ec7b(IP), AX
  0x54a4c5		4c89fb			MOVQ R15, BX
  0x54a4c8		4889d9			MOVQ BX, CX
  0x54a4cb		e8f0ddf3ff		CALL runtime.makeslice(SB)
[…]

An easy speed-up was to avoid allocations through reuse (in goturbopfor ). In the DCS pfordec package (with the improved API design ), I ended up with a vals [256]uint32 field in the StreamDecoder type, which brings us from 773 Mval/s to 858 Mval/s on the debian-mix:

% benchstat -filter '/impl:go /vals:debian-mix .unit:(Mval/s)' \
  baseline.txt bench.txt
goos: linux
goarch: amd64
pkg: github.com/Debian/dcs/internal/turbopfor/pfordec
cpu: AMD Ryzen 9 9950X3D 16-Core Processor
           │ baseline.txt │             bench.txt              │
           │    Mval/s    │   Mval/s     vs base               │
n=2048        1.089k ± 1%   1.175k ± 0%   +7.85% (p=0.002 n=6)
n=2039         974.7 ± 0%   1046.0 ± 0%   +7.32% (p=0.002 n=6)
n=160          434.9 ± 1%    513.6 ± 5%  +18.11% (p=0.002 n=6)
geomean        772.9         857.7       +10.98%

Aside from the speed-up, avoiding memory allocations is generally nice in benchmarks because it removes the garbage collector from the equation and makes it less likely that your benchmarks get other processes OOM-killed on the same machine.

Generics for bit width specialization

In general, we want to make it easy for the compiler to understand as much as possible about our algorithm. Consider this bitpack implementation:

func bitpack(dest []byte, vals []uint32, bitWidth int) []byte {
  mask := uint32(1<<bitWidth - 1)
  var acc uint64
  var have int
  for _, val := range vals {
    acc |= uint64(val&mask) << have
    have += bitWidth
    for have >= 32 {
      dest = binary.LittleEndian.AppendUint32(dest, uint32(acc))
      acc >>= 32
      have -= 32
    }
  }
  for have > 0 {
    dest = append(dest, byte(acc))
    acc >>= 8
    have -= 8
  }
  return dest
}

Let’s think through what determines the iterations and control flow this function uses:

  1. The number of input values ( vals ), but not their actual value.
  2. The bit width to pack into ( bitWidth ).

With a bit of careful rearrangement, we can provide the compiler with both, a fixed number of input values (say, 32), and a bit width, both known at compile time. Why is this worthwhile? Because we can manually unroll the loop, let the compiler eliminate much of the repetition and get much faster compiled code as a result!

Let’s first fix the number of input values to 32 and rewrite the loop to calculate the position offsets within dest instead of changing dest on each value (with AppendUint32 ):

func bitpack32Unrolled(dest []byte, vals *[32]uint32, bitWidth int) {
  // only one bounds check for 32 values
  dest = dest[: 4*bitWidth : 4*bitWidth]
  mask := uint32(1<<bitWidth - 1)
  var acc uint64
  var have, pos int
  // Manually unrolled loop starts here.
  // Each iteration is identical except for the vals[x] index.
  acc |= uint64(vals[0]&mask) << have
  have += bitWidth
  if have >= 32 {
    binary.LittleEndian.PutUint32(dest[pos:pos+4], uint32(acc))
    pos += 4
    acc >>= 32
    have -= 32
  }

  // vals[1] .. vals[30] elided for brevity

  // Each loop iteration is 8 lines of Go code, so for 32 input values,
  // bitpack32Unrolled contains 8*32 = 256 lines of code.

  acc |= uint64(vals[31]&mask) << have
  have += bitWidth
  if have >= 32 {
    binary.LittleEndian.PutUint32(dest[pos:pos+4], uint32(acc))
    pos += 4
    acc >>= 32
    have -= 32
  }

  // have == 0; for all bitWidths
}

Next, we want to specialize not just for 32 input values, but also for each of the 32 bit widths.

Can we do better than hand-copying bitpack32Unrolled 32 times (= 8192 lines of Go code)?

Yes, we can use Go generics to help us with the code generation!

In Go, array types like [4]byte (not slices like []byte !) contain the length of the array as part of their type, meaning [1]byte (an array of length 1) is a different type than [2]byte .

Instead of passing the bit width as a function parameter, we can declare 32 different types (one for each bit width) and recover the bit width (at compile time!) from the type system:

type bitWidthT interface {
  [1]byte | [2]byte | [3]byte | [4]byte | [5]byte |
  [6]byte | [7]byte | [8]byte | [9]byte | [10]byte |
  [11]byte | [12]byte | [13]byte | [14]byte | [15]byte |
  [16]byte | [17]byte | [18]byte | [19]byte | [20]byte |
  [21]byte | [22]byte | [23]byte | [24]byte | [25]byte |
  [26]byte | [27]byte | [28]byte | [29]byte | [30]byte |
  [31]byte | [32]byte
}

func bitpack32Unrolled[T bitWidthT](dest []byte, vals *[32]uint32) {
  var zero T
  bitWidth := len(zero)                  // known at compile time
  dest = dest[: 4*bitWidth : 4*bitWidth] // make cap known at compile time
  mask := uint32(1<<bitWidth - 1)
  var acc uint64
  var have, pos int
  // Manually unrolled loop starts here.
  // Each iteration is identical except for the vals[x] index.
  acc |= uint64(vals[0]&mask) << have
  have += bitWidth
  if have >= 32 {
    binary.LittleEndian.PutUint32(dest[pos:pos+4], uint32(acc))
    pos += 4
    acc >>= 32
    have -= 32
  }

  // vals[1] .. vals[31] elided for brevity
}

When we instantiate bitpack32Unrolled[bitWidthT] with all 32 different types ( [1]byte , [2]byte , …, [32]byte ), the compiler substitutes the bitWidthT type parameter and produces 32 copies of the function, which we can find in our compiled executable with names like github.com/Debian/dcs/internal/turbopfor/pforenc.bitpack32Unrolled[go.shape.[12]uint8] . The “shape” of a generic type is based on its memory layout, so a shape for [1]byte must be different than the shape for [2]byte .

Because the bitWidth is now known at compile time, the Go compiler can generate close to the optimal machine code for each bit width, which we can confirm using go tool objdump .

The code is branchless (after the one bounds check per 32 values) and aside from the loads and stores (from/to memory) consists only of shifts and bit operations, all with constant operands:

% go test -c && go tool objdump -S pforenc.test
[…]
TEXT github.com/Debian/dcs/internal/turbopfor/pforenc.bitpack32Unrolled[go.shape.[28]uint8](SB) /home/michael/dcs/internal/turbopfor/pforenc/bitpackunroll.go
func bitpack32Unrolled[T bitWidthT](dest []byte, vals *[32]uint32) {
  0x660580              55                      PUSHQ BP
  0x660581              4889e5                  MOVQ SP, BP
  0x660584              48895c2418              MOVQ BX, 0x18(SP)
        dest = dest[: 4*bitWidth : 4*bitWidth] // make cap known at compile time
  0x660589              4883ff70                CMPQ DI, $0x70
  0x66058d              0f820b030000            JB 0x66089e
        acc |= uint64(vals[0]&mask) << have
  0x660593              8b06                    MOVL 0(SI), AX
  0x660595              25ffffff0f              ANDL $0xfffffff, AX
        acc |= uint64(vals[1]&mask) << have
  0x66059a              8b4e04                  MOVL 0x4(SI), CX
  0x66059d              81e1ffffff0f            ANDL $0xfffffff, CX
  0x6605a3              48c1e11c                SHLQ $0x1c, CX
  0x6605a7              4809c8                  ORQ CX, AX
                acc >>= 32
  0x6605aa              4889c1                  MOVQ AX, CX
  0x6605ad              48c1e820                SHRQ $0x20, AX
                binary.LittleEndian.PutUint32(dest[pos:pos+4], uint32(acc))
  0x6605b1              90                      NOPL
        b[0] = byte(v)
  0x6605b2              890b                    MOVL CX, 0(BX)
        acc |= uint64(vals[2]&mask) << have
  0x6605b4              8b4e08                  MOVL 0x8(SI), CX
  0x6605b7              81e1ffffff0f            ANDL $0xfffffff, CX
  0x6605bd              48c1e118                SHLQ $0x18, CX
  0x6605c1              4809c1                  ORQ AX, CX
                acc >>= 32
  0x6605c4              4889c8                  MOVQ CX, AX
  0x6605c7              48c1e920                SHRQ $0x20, CX
                binary.LittleEndian.PutUint32(dest[pos:pos+4], uint32(acc))
  0x6605cb              90                      NOPL
        b[0] = byte(v)
  0x6605cc              894304                  MOVL AX, 0x4(BX)

Now we need to actually call bitpack32 from the general bitpack function:

func bitpack(dest []byte, vals []uint32, bitWidth int) []byte {
  if bitWidth == 0 {
    return dest // no payload, sparse block with only exceptions
  }
  if len(vals) >= 32 {
    size := 4 * bitWidth
    for len(vals) >= 32 {
      existing := len(dest)
      dest = slices.Grow(dest, size)[:existing+size]
      bitpack32(dest[existing:] /*append*/, (*[32]uint32)(vals), bitWidth)
      vals = vals[32:]
    }
  }
  mask := uint32(1<<bitWidth - 1)
  var acc uint64
  var have int
  for _, val := range vals {
    acc |= uint64(val&mask) << have
    have += bitWidth
    for have >= 32 {
      dest = binary.LittleEndian.AppendUint32(dest, uint32(acc))
      acc >>= 32
      have -= 32
    }
  }
  for have > 0 {
    dest = append(dest, byte(acc))
    acc >>= 8
    have -= 8
  }
  return dest
}

func bitpack32(dest []byte, vals *[32]uint32, bitWidth int) {
  switch bitWidth {
  case 1: bitpack32Unrolled[[1]byte](dest, vals)
  case 2: bitpack32Unrolled[[2]byte](dest, vals)
  case 3: bitpack32Unrolled[[3]byte](dest, vals)
  case 4: bitpack32Unrolled[[4]byte](dest, vals)
  case 5: bitpack32Unrolled[[5]byte](dest, vals)
  case 6: bitpack32Unrolled[[6]byte](dest, vals)
  case 7: bitpack32Unrolled[[7]byte](dest, vals)
  case 8: bitpack32Unrolled[[8]byte](dest, vals)
  case 9: bitpack32Unrolled[[9]byte](dest, vals)
  case 10: bitpack32Unrolled[[10]byte](dest, vals)
  case 11: bitpack32Unrolled[[11]byte](dest, vals)
  case 12: bitpack32Unrolled[[12]byte](dest, vals)
  case 13: bitpack32Unrolled[[13]byte](dest, vals)
  case 14: bitpack32Unrolled[[14]byte](dest, vals)
  case 15: bitpack32Unrolled[[15]byte](dest, vals)
  case 16: bitpack32Unrolled[[16]byte](dest, vals)
  case 17: bitpack32Unrolled[[17]byte](dest, vals)
  case 18: bitpack32Unrolled[[18]byte](dest, vals)
  case 19: bitpack32Unrolled[[19]byte](dest, vals)
  case 20: bitpack32Unrolled[[20]byte](dest, vals)
  case 21: bitpack32Unrolled[[21]byte](dest, vals)
  case 22: bitpack32Unrolled[[22]byte](dest, vals)
  case 23: bitpack32Unrolled[[23]byte](dest, vals)
  case 24: bitpack32Unrolled[[24]byte](dest, vals)
  case 25: bitpack32Unrolled[[25]byte](dest, vals)
  case 26: bitpack32Unrolled[[26]byte](dest, vals)
  case 27: bitpack32Unrolled[[27]byte](dest, vals)
  case 28: bitpack32Unrolled[[28]byte](dest, vals)
  case 29: bitpack32Unrolled[[29]byte](dest, vals)
  case 30: bitpack32Unrolled[[30]byte](dest, vals)
  case 31: bitpack32Unrolled[[31]byte](dest, vals)
  case 32: bitpack32Unrolled[[32]byte](dest, vals)
  }
}

Encoding remainder blocks is quite a bit faster (full blocks use the vertical layout anyway):

% benchstat -filter '/impl:go /n:160 .unit:(Mval/s)' baseline.txt bench.txt
goos: linux
goarch: amd64
pkg: github.com/Debian/dcs/internal/turbopfor/pforenc
cpu: AMD Ryzen 9 9950X3D 16-Core Processor
                         │ baseline.txt │             bench.txt              │
                         │    Mval/s    │   Mval/s     vs base               │
vals=bitpacking-bw1          751.2 ± 3%   1120.5 ± 0%  +49.15% (p=0.002 n=6)
vals=bitpacking-bw2          716.8 ± 2%   1176.0 ± 0%  +64.07% (p=0.002 n=6)
vals=bitpacking-bw7          700.0 ± 1%   1078.5 ± 0%  +54.08% (p=0.002 n=6)
vals=bitpacking-bw1-exc      524.8 ± 1%    736.8 ± 0%  +40.40% (p=0.002 n=6)
vals=bitpacking-bw2-exc      543.7 ± 1%    758.2 ± 0%  +39.46% (p=0.002 n=6)
vals=bitpacking-bw7-exc      566.7 ± 1%    787.7 ± 0%  +38.99% (p=0.002 n=6)
vals=bitpacking-vb-exc       442.6 ± 1%    616.5 ± 0%  +39.29% (p=0.002 n=6)
vals=sparse-exc              532.4 ± 0%    787.8 ± 0%  +47.97% (p=0.002 n=6)
vals=sparse-vb-exc           408.9 ± 1%    597.8 ± 0%  +46.20% (p=0.002 n=6)
vals=debian-mix              559.5 ± 0%    783.8 ± 9%  +40.09% (p=0.002 n=6)

This performance win comes at the cost of binary size increase. In this case, the .text section (executable code) grows by about 20 KB and the .gopclntab section grows by another 26 KB. Definitely a price I am very willing to pay, but the case might not be as clear in all circumstances.

Optimization: Bigger strides with SIMD

Even without reaching for SIMD instructions, a TurboPFor implementation can be made faster by making it work bigger strides. Take this code from the goturbopfor teaching decoder which counts the number of exceptions by checking if each value’s bit is set in the exception bitmap:

case blockBitpackingExceptions:
  bx, input := input[0], input[1:]
  n := len(output)

  exmap, input := input, input[(n+7)/8:]
  nex := 0 // number of exceptions
  for i := range n {
    if exmap[i/8]&(1<<uint(i%8)) != 0 {
      nex++
    }
  }
  exceptions := d.scratch[:nex]

We can use the bits.OnesCount64 functions to count ones bits in the exception bitmap, 64 values at a time. For remainder blocks, the rest is processed 8 values (1 byte) at a time:

i := 0
for ; i+8 <= n/8; i += 8 {
  xm8 := binary.LittleEndian.Uint64(exmap[i:])
  nex += bits.OnesCount64(xm8)
}
for ; i < (n+7)/8; i++ {
  xmb := exmap[i]
  // Clear the bits which do not belong to the exception map:
  if rem := n - i*8; rem < 8 {
    xmb &= 1<<rem - 1
  }
  // Go compiles OnesCount32 into an intrinsic,
  // but not OnesCount8, so we convert to uint32:
  nex += bits.OnesCount32(uint32(xmb))
}

OnesCount64 uses a 64-bit register. For comparison, AVX2 SIMD instructions use 256-bit registers (= 8 uint32 ) and AVX512 SIMD instructions use 512-bit registers.

In the following sections, we will first set up our build tags for conditional compilation to use a trivial SIMD instruction, then walk through an AVX2 and AVX512 SIMD kernel.

SIMD build tags

Let’s assume we have the following scalar code:

constant.go :

package pfordec

func fillConstant(output []uint32, val uint32) {
  for i := range output {
    output[i] = val
  }
}

To increase throughput, we can use AVX2 instructions if they are available on the CPU on which the program runs, i.e. using runtime dispatch. We’ll first rename fillConstant to fillConstantScalar (it’s now the fallback path):

constant.go :

package pfordec

func fillConstantScalar(output []uint32, val uint32) {
  for i := range output {
    output[i] = val
  }
}

Next, we’ll supply two different implementations ( constant_nosimd.go and constant_amd64.go ), the latter of which is selected when compiling for GOARCH=amd64 with GOEXPERIMENT=simd (the latter will hopefully be dropped in a later version of Go). The nosimd variant just dispatches to the fillConstantScalar , which will likely be inlined:

//go:build !goexperiment.simd || !amd64

package pfordec

func fillConstant(output []uint32, val uint32) {
  fillConstantScalar(output, val)
}

The constant_amd64.go variant assigns the hasAVX2 global variable by doing a CPUID check and then jumps to the scalar fallback if !hasAVX2 , i.e. the CPU is too old:

//go:build goexperiment.simd && amd64

package pfordec

import "simd/archsimd"

var hasAVX2 = archsimd.X86.AVX2()

func fillConstant(output []uint32, val uint32) {
  if !hasAVX2 {
    fillConstantScalar(output, val)
    return
  }
  val8 := archsimd.BroadcastUint32x8(val)
  i := 0
  for ; i+8 <= len(output); i += 8 {
    val8.StoreArray((*[8]uint32)(output[i : i+8]))
  }
  // use the scalar implementation for the last <= 7 elements
  fillConstantScalar(output[i:], val)
}

We can go one step further by conditionally compiling const hasAVX2 = true when GOAMD64 is set to v3 or higher (i.e. the amd64.v3 build tag is set). As a practical example from Debian Code Search, we currently need the following checks / dispatches:

code function vector instruction set GOAMD64
encoder bitpack256v AVX2 GOAMD64=v3
encoder exbitmap AVX512 GOAMD64=v4
encoder scan AVX512+VBMI+GFNI+BITALG n/a
decoder bitunpack AVX2 GOAMD64=v3
decoder bitunpack256v32 AVX2 GOAMD64=v3
decoder bitunpack256v32Ex AVX512 GOAMD64=v4

In DCS, the effect is measurably positive, but small .

The 256 uint32 vertical layout

First, here is the layout explanation from my 2019 TurboPFor analysis blog post :

In regular (non-SIMD) bitpacking , integers are stored on disk one after the other, padded to a full byte, as a byte is the smallest addressable unit when reading data from disk. For example, if you bitpack only one 3 bit int, you will end up with 5 bits of padding.

SIMD bitpacking works like regular bitpacking, but processes 8 uint32 little-endian values at the same time, leveraging the AVX instruction set . The following illustration shows the order in which 3-bit integers are decoded from disk:

The scalar implementation uses an array of 8 uint64 to process 8 values at a time:

func bitunpack256v32(input []byte, dest []uint32, bitWidth int) (read int) {
  mask := uint64(1)<<bitWidth - 1
  orig := len(input)
  var bits uint
  var acc [8]uint64 // accumulator: current+next bits
  for op := 0; op < len(dest); {
    if bits < uint(bitWidth) {
      // read 8 more uint32s
      for i := range 8 {
        acc[i] |= uint64(binary.LittleEndian.Uint32(input)) << bits
        input = input[4:]
      }
      bits += 32
    }
    for i := range 8 {
      dest[op] = uint32(acc[i] & mask)
      op++
      acc[i] >>= bitWidth
    }
    bits -= uint(bitWidth)
  }
  return orig - len(input)
}

The SIMD version also processes 8 values, but without a for i := range 8 loop!

One difference is that we no longer have the luxury of using uint64 for acc (holding rest and current bits); because AVX2 registers only fit 8 uint32 (not 8 uint64 ). Instead, we split acc into rest8 and cur8 .

func bitunpack256v32(fullinput []byte, fulldest []uint32, bitWidth int) (read int) {
  dest := fulldest[:256]
  if bitWidth == 0 {
    clear(dest)
    return 0
  }
  n := 32 * int(bitWidth)
  input := fullinput[:n] // tell the Go compiler how long the input is
  mask8 := archsimd.BroadcastUint32x8(uint32(1)<<bitWidth - 1)
  bitWidth8 := archsimd.BroadcastUint32x8(uint32(bitWidth))
  var bits uint
  pos := 0
  // var acc [8]uint64
  var rest8 archsimd.Uint32x8
  var cur8 archsimd.Uint32x8
  for op := 0; op < 256; op += 8 {
    if bits < uint(bitWidth) {
      // read 8 more uint32s
      // acc[i] |= uint64(binary.LittleEndian.Uint32(input)) << bits
      next := archsimd.LoadUint8x32(input[pos : pos+32]).ReshapeToUint32s()
      pos += 32  // input = input[4:]
      cur8 = rest8.Or(next.ShiftAllLeft(uint64(bits)))
      // acc[i] >>= bitWidth
      rest8 = next.ShiftAllRight(uint64(uint(bitWidth) - bits))
      bits += 32
    } else {
      cur8 = rest8
      // acc[i] >>= bitWidth
      rest8 = rest8.ShiftRight(bitWidth8)
    }
    // dest[op] = uint32(acc[i] & mask)
    cur8.And(mask8).Store(dest[op : op+8])
    bits -= uint(bitWidth)
  }
  return n
}

The SIMD version benchmarks about 3x as fast as the scalar version.

Another significant speedup is to use generics for bit width specialization for this SIMD kernel so that bitWidth becomes a compile-time constant and the compiler can generate better code.

Positional Popcount

For my TurboPFor encoder, I implemented the same techniques as described above:

  1. Bitpack full blocks with SIMD (AVX2)

  2. Gather exceptions using SIMD (AVX512)

  3. Use generics to specialize per bit width

These changes are sufficient to roughly match the cgo performance, but then Claude Fable 5 found another 2x speed-up on top of that !

The key observation is that once encoding blocks is fast, the preceding step of scanning the input values to decide which block type to use becomes the bottleneck. Here is the encoder’s main encode function, which first does one pass over the input values ( scan ) and then prices all different block types at all relevant bit widths (requires fast access to the scan histogram):

func (be *BlockEncoder) encode(dest []byte, vals []uint32, layout blockLayout) []byte {
  var stats stats
  scan(&stats, vals) // gathers statistics from every value in vals
  bitWidth := bits.Len32(stats.or)
  if stats.or == stats.and {
    return be.encodeConstant(dest, vals, bitWidth)
  }
  n := len(vals)
  // bitpacking is the default, unless we find a more efficient block type.
  bestType := blockBitpacking
  bestB := bitWidth
  best := priceBitpack(n, bitWidth, layout)

  // Walk from high bitWidths to low: to break ties, we prefer
  // the encoding with fewer exceptions (for faster decoding).
  for b := bitWidth - 1; b >= 0; b-- { // up to 32 iterations
    nex := int(stats.cnt[b])
    size := priceBitpackExceptions(n, b, bitWidth, nex, layout)
    if size < best {
      bestType = blockBitpackingExceptions
      bestB = b
      best = size
    }
    // Over-approximate the number of VB bytes.
    vb := nex + // exceptions using 1, 2, 3, 4, or 5 VB bytes
      int(stats.cnt[b+7]+ // exceptions using 2, 3, 4, or 5 VB bytes
        stats.cnt[b+14]+ // exceptions using 3, 4, or 5 VB bytes
        stats.cnt[b+19]+ // exceptions using 4 or 5 VB bytes
        stats.cnt[b+24]) // exceptions using 5 VB bytes
    size = headerBytes + headerExBytes + payloadBytes(n, b, layout) + vb + nex
    if size < best {
      bestType = blockBitpackingVBExceptions
      bestB = b
      best = size
    }
  }
  switch bestType {
  case blockBitpacking:
    return be.encodeBitpack(dest, vals, layout, bitWidth)
  case blockBitpackingExceptions:
    return be.encodeBitpackExc(dest, vals, layout, bestB, bitWidth-bestB)
  case blockBitpackingVBExceptions:
    return be.encodeBitpackVBExc(dest, vals, layout, bestB, int(stats.cnt[bestB]))
  default:
    panic("BUG: bestType not implemented")
  }
}

I’ll show you a slightly shortened version of scan , the function which is the bottleneck:

type stats struct {
  // cnt[n] = how many values where bits.Len32(val)>n,
  // i.e. how many exceptions are required for bitWidth=n.
  // Padded so that cnt[b+24] is always in bounds.
  cnt [32 + 24]uint32
}

func scan(output *stats, vals []uint32) {
  for _, val := range vals {
    for b := range bits.Len32(val) {
      output.cnt[b]++ // b bits are not enough to store val
    }
  }
}

Let’s consider the following 3 example values to understand the resulting cnt :

input input (bin) bits.Len32
23 0b0000010111 5
5 0b0000000101 3
666 0b1010011010 10

The resulting cnt exception count histogram would contain ( cnt shortened to c ):

c[0] c[1] c[2] c[3] c[4] c[5] c[6] c[7] c[8] c[9] c[10]
3 3 3 2 2 1 1 1 1 1 0

In words, this means that at bit width 10, we could encode all the values without any exceptions.

But most values do not need 10 bits, so a bit width of 5 would be more efficient, but requires storing one exception. Encoding at bit width 4 requires 2 exceptions, and so on.

The scan function above is intentionally kept simple for illustration. We can make it faster by moving the per-bit-width loop outside the per-element loop . The fast version still needs about 12 instructions per value. With SIMD, we can reduce this to by 8x to only 1.5 instructions per value!

The trick: smear masks enable positional popcount

The trick is to turn each input value into its “smear mask” (imagine taking the first 1 bit and smearing it across the remaining positions). Here are the smear masks for our example:

input input (bin) bits.Len32 “smear mask”
23 0b0000010111 5 0b0000011111
5 0b0000000101 3 0b0000000111
666 0b1010011010 10 0b1111111111

Turning a value into its smear mask is computationally cheap: Go implements BitLen(x) (functions like bits.Len32 ) by calculating 32 - LZCNT(x) . We can calculate the “smear mask” of a value with ^uint32(0) >> LZCNT(x) , i.e. starting with a 32-one-bits mask and shifting it by the number of leading zeros.

Now, to obtain e.g. cnt[4] , we can count the 1 bits at bit position 4 of all input values.

The POPCNT instruction counts bits very efficiently, but it counts one bits within a register, so it counts rows, not columns. Counting columns is called Positional Population Count .

I found the following papers that describe positional popcount with SIMD:

Positional Popcount: a visual explanation

To understand the AVX512 implementation of positional popcount, I found it most helpful to visualize an AVX512 register (512 bits, i.e. 64 bytes). The graphic below uses the Uint64x8 layout, meaning it divides the register into 8 lanes of 64 bits (= 8 bytes) each.

This illustration shows the whole process: how uint32 s are loaded into an AVX512 register (all 4 of its bytes, in sequence) and where we end up, i.e. the 32 positional popcounts:

Let’s break down this process into its individual steps.

First, we turn each loaded value into its smear mask as explained above.

The VPOPCNTB vector instruction calculates POPCNT (1 byte) of 64 bytes at once, but first we need to shuffle the bytes inside the register: in load order, we have a full uint32 (4 bytes), followed by another uint32 , per lane. First, we permute the bytes ( VPERMB ) such that all the first bytes of each value end up in one lane (“transpose the bytes”):

Next, we “transpose the bits” using the GF2P8AFFINEQB instruction, which sounds scary but turns out to be quite flexible for bit manipulation of all kinds. The GF2P8AFFINEQB instruction is also “the star of the show” in Go’s Green Tea Garbage Collector (2025). Here is the bit transpose, shown in the AVX512 register layout (see below for a different layout):

I found it easier to understand the transpose step when arranging the 8 bytes of lane 0 from top-to-bottom (instead of left-to-right), because then it looks like a 90 degree clockwise rotation:

Now we can use VPOPCNTB to count the bits in all 64 bytes at once:

After all loop iterations (processing 16 values each) are done, we add the two groups (first 8 values, second 8 values) to obtain the 32 exception counts:

Positional Popcount: Go SIMD

Here is the Go code that implements what I described visually above:

func scanSIMD(output *stats, vals []uint32) {
  ones16 := archsimd.BroadcastUint32x16(^uint32(0)) // 16 32-one-bits masks
  shuffle := archsimd.LoadUint8x64Array(&scanShuffle)
  units := archsimd.LoadUint8x64Array(&scanUnits)
  var acc archsimd.Uint8x64
  idx := 0
  for ; idx+16 <= len(vals); idx += 16 {
    v := archsimd.LoadUint32x16(vals[idx : idx+16])
    // Replace all values with their smear masks.
    smear := ones16.ShiftRight(v.LeadingZeros()).ReshapeToUint8s()
    // Transpose: shuffle the bytes, then transpose the bits.
    matrices := smear.Permute(shuffle).ReshapeToUint64s()
    transposed := units.GaloisFieldAffineTransform(matrices, 0)
    // Popcount 64 bytes at once into the accumulator.
    acc = acc.Add(transposed.OnesCount())
  }
  // Store the accumulator into output.cnt:
  // Widen the two groups of byte counts to uint16 lanes (so that
  // 128+128 = 256 fits), fold them into cnt[b] for b=0..31,
  // then widen again to the uint32 lanes of output.cnt.
  sum := acc.GetLo().ExtendToUint16().Add(acc.GetHi().ExtendToUint16())
  sum.GetLo().ExtendToUint32().Store(output.cnt[0:16])
  sum.GetHi().ExtendToUint32().Store(output.cnt[16:32])
  // scalar tail for the 0..15 remaining values
  for _, val := range vals[idx:] {
    for b := range bits.Len32(val) {
      output.cnt[b]++
    }
  }
}

Have a look at the commit introducing positional popcount to DCS for the full code (including shuffle tables and ISA checks) as well as the detailed benchmark results.

Go even faster?

The SIMD optimizations I showed above beat the cgo TurboPFor library that Debian Code Search used before. When comparing apples to apples, i.e. backporting the AVX512 kernels and positional popcount technique to C TurboPFor, Go benchmarks a little slower at ≈1.4x C.

Could we make my Go TurboPFor implementation even faster, to truly match the C speed?

Yes! But also no. Let me explain:

  1. We could use more SIMD instructions to remove all code that still processes one value at a time. For example, in my encoder’s encodeBitpackVBExc function. Or we could price all bit widths concurrently in encode . Or in the decoder’s exception apply code path.
    But all of these SIMD instructions make understanding (and changing) the code harder, so I am cautious regarding which ones I introduce.

  2. A big part of the performance gap is due to Go’s bounds checks . While it costs performance, bounds checking is great for safety, so I will not turn off bounds checking. The Go compiler eliminates a number of bounds checks when it understands it’s safe to do so. One optimization avenue could be to make the prove pass in the Go compiler smarter to eliminate more bounds checks.

  3. When doing mid-stack inlining (proposal #19348) (2017), Go sometimes needs to put NOP instructions into the binary so that it can attach inlining markers. For dispatch-bound functions, these extra NOPs can measurable slow down execution.

  4. The Go compiler currently allows specifying the architecture ( GOARCH=amd64 ) and microarchitecture ( GOAMD64=v3 ), but not a specific CPU architecture (like AMD Zen 4). Therefore, CPU-specific workarounds for one vendor affect all the generated code. The specific one I encountered in my code is that the Go compiler emits XORL CX,CX before every POPCNT to break a false-output-dependency from the Intel Sandy Bridge Skylake era, which is unnecessary on AMD Zen CPUs.
    I suspect that Go intentionally does not offer this level of customizability.

  5. After all of the above points are addressed, what remains is better code generation in specific cases. To illustrate what I mean, consider the example of incrementing a loop variable, where Go re-derives an index every time:
    Go: POPCNTL; ADDQ DI,CX; LEAQ (base)(CX*4) (3 instructions)
    clang: popcnt; lea rax,[rax+4*rdi] (2 instructions)
    Depending on the specific case, improving the compiler might be easy or prohibitively complex. Often, such improvements are hard to measure conclusively.

Conclusion

Go’s SIMD support makes available — in Go code without having to resort to cgo or assembly — a powerful part of modern CPUs which allows speeding up the kind of computation that TurboPFor needs by an order of magnitude! 😲

I found it very valuable to use a coding agent (Claude Code, with Opus 5 and Fable 5 in this case) to help with the many tedious parts of such performance work (and still it took me weeks!). The LLM can read objdump output much faster than I can, can see patterns and correlations I might never identify, never becomes frustrated after a compiler error or runtime panic, and never runs out of patience to run one more experiment, as long as I give it measurable and reachable goals.

The performance of the SIMD code which one can get from the Go compiler is pretty close to what a good C compiler like clang provides. The CPU performance counters show value decoding speeds of 7 instructions/cycle (IPC) on a machine where the maximum is 8 IPC.

To me, SIMD support is a very welcome addition to Go.

Did you like this post? Subscribe to this blog’s RSS feed to not miss any new posts!

I run a blog since 2005, spreading knowledge and experience for over 20 years! :)

All of my content is human-authored. I do use LLMs for research and knowledge work, and even to review my posts, but all writing is my own, every word is my own voice.

Online bookies accused of UK privacy breaches with use of cookie banners

Guardian
www.theguardian.com
2026-09-06 03:00:01
Most gambling websites nudge users towards accepting tracking data and harvest it before consent is given, study shows Online bookmakers and casinos have been accused of widespread non-compliance of information privacy requirements, including “data surveillance” of customers, according to a study. N...
Original Article

Online bookmakers and casinos have been accused of widespread non-compliance of information privacy requirements, including “data surveillance” of customers, according to a study.

Nearly nine out of 10 (86%) licensed British gambling websites appear to be flouting the general data protection regulation (GDPR), the strict rules governing how organisations can collect, store and process personal data, according to the research.

The findings relate to the “cookie” banners that appear on a website when a user first navigates to it, asking which information they are willing to share.

Britain’s data privacy regulator, the Information Commissioner’s Office, is in the midst of a multi-year project to force websites to comply with GDPR rules governing the banners. It claims to have forced 95% of the top 1,000 websites in the country to comply with the cookie and tracking regulations.

But a study by researchers at the University of Swansea’s GREAT Centre found that big operators in the gambling industry appear to lag far behind.

Nearly a quarter (24%) of 624 gambling websites tested did not offer the option to turn off tracking software, which allows advertisers to follow users around the web and target them with marketing tailored to their interests.

Operators that did not provide an option to turn off tracking included the Brentford FC sponsor Hollywood Bets, and Admiral Casino, owned by the high-street slot machine firm of the same name.

Two-thirds of operators began harvesting users’ data before they had given their consent for it to be collected, the study found. They included well-known operators such as Ladbrokes and William Hill. Operators are allowed to do this for legitimate reasons, such as ensuring a customer is logging on from the UK, but researchers found that data was sent to third-party analytics platforms used for marketing.

Of the websites studied, 2% offered no consent choice at all, including Dafabet, the sponsor of Celtic FC.

Researchers also found that the vast majority of bookies and online casinos use “dark patterns” to nudge people towards accepting data sharing. These nudges include visual emphasis of the least privacy-friendly option (60%), default pre-selection of privacy-unfriendly settings (29%), and the reject option being hidden behind a second layer (47%).

Such patterns do not necessarily constitute breaches in themselves. But the same proportion of websites that feature them, 86%, appear to have committed at least one breach of GDPR, the study found.

This proportion is significantly higher than has been reflected in analysis of the wider internet. A previous study that examined all types of website, not just gambling, placed the figure at 54%.

Ravi Naik, legal director at the data protection specialist AWO, said the report’s findings “paint a picture of widespread and systemic non-compliance”.

He added: “It is sadly no surprise to see the findings in this report, yet the consequences of non-compliance are no less damaging.

“The most striking thing to arise from this report is the light it casts on the failure of the Information Commissioner’s Office to take meaningful enforcement action against the online gambling sector.”

AWO has previously acted for the campaign group Clean Up Gambling , which raised concerns with the ICO about gambling firms’ compliance.

In 2024, SkyBet was reprimanded by the ICO for unlawfully sharing users’ data with advertising companies, after the campaign raised concerns, through AWO, about an operator that treated a customer’s gambling during early-morning hours as a sign of harm and as a cue to send personalised inducements at those times.

SkyBet was not among those claimed to have breached GDPR in the University of Swansea report.

The study’s authors said the goal of collecting users’ data was “maintaining engagement and consumer losses”.

“The particular risk posed by data surveillance in online gambling, given the structural overlap between profitable behavioural patterns and harmful gambling behaviours, underscores the importance of data consent design as a consumer protection issue.”

An ICO spokesperson said the data regulator was committed to “monitoring compliance across the UK’s most visited websites and driving long-term adherence to lawful cookie practices”.

They added: “We will take action where necessary to protect people’s information rights.”

Evoke, the owner of William Hill, declined to comment. Entain, the owner of Ladbrokes, said any data it collected prior to consent being given was not used for advertising or marketing. Hollywood Bets and Admiral Casino did not return a request for comment.

Site Is Closed on Sundays

Hacker News
v7.robweychert.com
2026-09-06 02:23:05
Comments...
Original Article

Hi, I’m Rob Weychert.

I make art and design , obsess over film and music , hoard trivial archival data , and share it all on this here website. Enjoy your stay.

More about me →

Featured post

Backfilling metadata

Six thousand tweets. Ten months. One taxonomy.

Go to post

Incomplete Open Cubes Revisited poster

One poster, 4,094 variations on an incomplete open cube

Go to product

Featured post

Typographic scales and technical pens

A flexible system for consistent stroke widths across type sizes

Go to post

Beyond Tellerrand Berlin 2022

An opening title sequence for a design and tech conference

Go to project

How To: Federated Stars

Lobsters
domaindrivenarchitecture.org
2026-09-06 02:05:41
Comments...
Original Article

With the final PR the first star federation feature will be completely added to Forgejo. You can check out the PR that contains an overview over the new feature: https://codeberg.org/forgejo/forgejo/pulls/1680

The feature allows you to define following repositories such as origin of mirrors or forks. If then the mirror or fork gets a star by someone, the origin repository will also get a star via federated ActivityPub like activity. Please note, however, that for now only the star activity is federated. The unstar activity is not yet federated.

How to use it:

  1. Enable federation, by setting setting.Federation.Enabled = true in your config.
  2. Declare the repositories that should follow yours:
    1. Go to your repository settings.
    2. As can be seen in the image below, add the URLs of the following repositories under “Federation Settings”. You can find the URLs of the following repositories displayed in their “Federation Settings”.

federation settings

With these settings everything is set up to federate the stars from your repository to the following repositories. In the following video you can see the star federation in action:

You can try it out yourself locally. Edit: With the official merge of federation features we removed our federation feature testing server.

Federation Forgejo Go

Ask HN: Would you read a statistics textbook?

Hacker News
news.ycombinator.com
2026-09-06 00:52:18
Comments...
Original Article

No idea about statistics, but in most physict courses in my university, they recomend 3 books:

1) The main book, that has a complete explanation and is well ordered. It's for learning.

2) Tha Landau book, that is super short and hard. It's only to check you didn't miss any important formula or topic.

3) There Feynman book, that is anassorted colection of fairytales for physicist. It's a pleasure to read it but you must already read 1 to understand it.

4) The Shaum book, that is almost a colection of exercices. Some people hate it. Some people love it. I like it as a companion to theother books.

I guess you are complaining that 1 is boring and want to write 3. It's a good idea, but it's harder than expected.

Beyond ORMs

Lobsters
noteflakes.com
2026-09-06 00:52:05
Comments...
Original Article

Beyond ORMs

05·09·2026

I’ve always been interested in the design of ORMs - I wanted to know how they work, what the different approaches are, and how they can be used. I tried a bunch of different ones, I even created one . But in the last few years I’ve been slowly gravitating toward working more directly with databases - issuing queries using plain SQL, processing raw data coming from the database, or building custom low-level abstractions for working with relational databases. This evolution in the way my code talks to databases is the reason I created Extralite , a Ruby gem for working with SQLite databases.

Why do I prefer direct interaction with the database instead of using an ORM? For any developer well-versed in SQL the limitations of ORMs should be obvious - there’s no support for features such as window functions, common table expressions (CTEs), or even the RETURNING clause; the ORM layer itself is a substantial dependency (the ActiveRecord codebase is about ~43KLoC), it imposes a performance cost in terms of memory and CPU time; and its defining feature, the mapping of rows to objects, is an anti-pattern that has been described as “ the Vietnam of computer science ”.

Now, I’m well aware of the fact that ActiveRecord is by far the most popular way to talk to a database in the Ruby/Rails community, and has greatly influenced many other ORMs for Ruby and other languages. But to me ActiveRecord has always felt wrong. The longer I work building web apps and platforms, the clearer it is to me. ActiveRecord provides the wrong abstraction for relational data. ActiveRecord concentrates on the single record, the idea that each record is an entity in and of itself. In reality, however, a normalized database will contain many tables with ancillary data, such as tags or labels, which does not need to be treated as entities. There’s time series data and event logs. Nowadays relational databases are even used as key-value stores and job queues. For these types of data, we’re frequently interested in record sets rather than individual records, and we don’t really need the entity abstraction. For those uses, ActiveRecord seems clumsy and wasteful.

Since ActiveRecord’s API revolves around the idea of a separate object for each row, descended from ActiveRecord::Base , with its hundreds of instance and class methods, programmers may get the idea that they’re actually dealing with discrete objects, and not entries in a record set. With ActiveRecord, a novice programmer might want to delete or update multiple records using #each , e.g.:

def make_bracelet(material)
  beads = Bead.where(material:).all
  bracelet = Bracelet.new(beads)
  beads.each(&:delete) # <== a separate DELETE query for each bead
  bracelet
end

In the example above, we issue one SELECT query to get the beads we’re interested in, and then for each post we’ll issue a DELETE query, so we got a N+1 situation. Of course, a simple solution would be to call delete_all instead of looping over the beads we read:

def make_bracelet(material)
  beads = Bead.where(material:).all
  bracelet = Bracelet.new(beads)
  Bead.where(material:).delete_all
  bracelet
end

So now we’re down to two queries, a SELECT query and a DELETE query, both of which have the same WHERE clause:

select * from beads where material = ?;
delete from beads where material = ?;

One problem with this is that we’re not guaranteed that those two queries will touch the same records, unless we run the two queries inside a transaction. A second problem is that we’re still doing two round-trips to the database. Can’t we both read and delete records in a single query? Yes we can, at least in databases such as PostgreSQL and SQLite, using the RETURNING clause:

delete from beads where material = ? returning *;

Running this query will delete the relevant records, and return their content. As far as I could tell, there’s no API for this kind of query in ActiveRecord. But all we need is a just a way to run a plain SQL query, which should be easy using Extralite:

def make_bracelet(material)
  beads = @db.query <<~SQL
    delete from beads where material = ? returning *
  SQL
  Bracelet.new(beads)
end

What we’ve achieved here is to reduce the interaction with the database to a single query that both deletes the relevant rows and returns their content. Yes, this means that we express queries in plain SQL, which might give you pause, but bear with me, I’m going somewhere with this.

The DSL Trap

One of the main selling points of ActiveRecord and ORMs in general is the convenience of a DSL: no need to write SQL, just use our magical DSL to construct any query you want. So in fact what happens is that you get a terrific general-purpose API with literally hundreds of instance and class methods, that allows you to dynamically build queries however you wish. But do we really need all those hundreds of methods?

Something I’ve observed in practically every web app codebase I ever looked at, is that, except in very rare cases, the number of different queries a given app will make is finite, and in fact relatively small. All those CRUD apps - written by CRUD monkeys 😉 - they basically just issue a few different types of queries. For example, a simple blog app might perform the following different queries:

Post.Create(title: 'foo', body: 'bar')            # create
post = Post.find(42)                              # read
post.save                                         # update
post.delete                                       # delete
Post.order_by(:stamp).all                         # list
Post.where(category: 'baz').order_by(:stamp).all  # list by category

Your app might want to bring in some associations, it might want to be able to filter and sort posts in different ways, so it’s going have to make a few other queries, but their number would average, I’d guess a rough estimate, maybe a dozen different queries per table. That, unless you’re building a full-featured visual query builder app!

So I think the question that should be asked is: if we only need to make a few dozen different queries, why should we use a DSL in the first place? Do we really need all of ActiveRecord’s magic and expressivness? Why not just express those queries directly in SQL? Compared to the amount of Ruby code in your app, the amount of SQL you’ll have to write is a drop in the bucket!

Now, one could argue that the biggest advantage of using ActiveRecord over writing plain SQL queries would be the lack of boilerplate and all the features you get for free, of which associations are perhaps the most important. Then again, in my opinion those abstractions have a way of crumbling under their own weight the moment you try to do something a bit more advanced or esoteric, such as incorporating non-entity data in your queries, or using window functions for example.

I think interacting with relational databases without having a basic grasp of SQL is not a good idea. Yes, vibe-coding has taken our profession by storm, and lots of people apparently think that the code doesn’t matter anymore and that we shouldn’t even look at it, and that we’re now all prompt monkeys and we should all just spend our day paying lots of money to Anthropic, watch our “quotas”, come up with creative ways to economize tokens (what a silly notion!) and building all those groovy exotic hyper-complex loops and harnesses and “skills” and AGENTS.md files and all that nonsense , instead of, you know, just writing normal code that works.

It is the present author’s opinion that understanding your code still matters, understanding what your database is doing still matters, performance still matters, and having at least a modicum of frugality in using compute resources still matters (actually it matters even more considering our present environmental challenges!)

Besides, I find it interesting that on one hand so much effort is being expended on making the Ruby runtime faster, yet developers who use Ruby on Rails seem to have such a cavalier, almost ignorant, attitude to performance: “who cares, compute is an infinite resource, the agent will take care of it, we’ll just tell it to make the code faster”. And indeed, who can fault them? As long as AI platform prices do not reflect the true cost of AI compute, why should people who vibe-code care at all about the performance of their own code (which they haven’t looked at!), why should they care about getting the most out of their compute infrastructure? In that sense, the absolutely amazing work done by a few very talented developers on making Ruby itself faster, is a sisyphean task, taking into account the ridiculous amount of unstoppable slop being added daily to actual Ruby on Rails apps. But I digress.

Rethinking the M in MVC

It has long been accepted that the M in MVC is supposed to be some kind of an ORM, a layer whose main responsibility is mapping table rows to entity objects, and providing an expressive API for getting a hold of such objects and manipulating them. But maybe instead of interacting with the database using a general-purpose query builder, we can come up with our own custom made API for interacting with the database. Let’s retake the example of a simple blog app, and imagine we had an interface that’s custom-made for dealing with posts:

posts = PostsStore.new(db)
id = posts.create(title: 'foo', body: 'bar')  # create
post = posts.by_id(id)                        # id
posts.update_by_id(id, title: 'FOO')          # update
posts.delete_by_id(id)                        # delete
posts.all                                     # list
posts.all_by_category(category: 'baz')        # list by category

Those are the same six queries as before, but the methods that return rows do so using plain Ruby hashes instead of custom objects. From the point of view of the app, this is just a change of interface, we’ve essentially created a bespoke API for reading and manipulating posts for our blog app. Just like with ActiveRecord, the whole database layer is abstracted away in a set of methods, the controller/business logic code doesn’t need to use a Post class with its deep, chainable API, it just calls methods on an interface.

The most important change, hoewever, is that whenever we deal with posts, we cannot just invent new queries, we have to use the ones that exist, or add new ones to the PostsStore class. The implementation of PostsStore is quite simple:

class PostsStore
  def initialize(db)
    @db = db
  end

  def create(title:, body:)
    @db.query_splat <<~SQL, title, body
      insert into posts (title, body)
      values (?, ?)
      returning id
    SQL
  end

  def by_id(id)
    @db.query_single_row <<~SQL, id
      select id, title, body
      from posts
      where id = ?
    SQL
  end

  ...

  def all
    @db.query <<~SQL
      select id, title, body, stamp
      from posts
      order by stamp desc
    SQL
  end
end

Here we’re taking advantage of one of Extralite’s defining features - the ability to extract data in however form you want, be it a single value, a single row, or a set of rows. Extralite also has some more advanced features, as I’ll show below.

Look at all the things we’re not doing: there are no entity objects, the data we want is returned from the database in the form we need, in plain Ruby hashes, Everything is explicit and easily understandable - the queries, the parameters, the columns. We’ve cut down substantially on the number of allocations we make. And of course, this kind of code can be easily scaffolded (for CRUD usage) or even generated by your favorite slop agent if you’re so disposed.

Also, look at how we removed all the unnecessary abstractions: there’s no entity classes, there’s no DSL driving the building of queries. We’re just talking to the database directly, with the help of Extralite, and we provide a convenient API that abstracts the database layer.

The Model Layer is an API

This design might seem baffling at first - where’s the ability to create queries on the fly as the app is developed? Where is the interaction with entities? Where do I put my business logic? The answer to all these questions is: the store class. The store class encapsulates everything that has to do with a certain kind of data. The store class should function as the interface for interacting with posts. Whatever you need to do with posts, it should be in the PostsStore class. For example, if you need to read posts with associations, just add a method that performs the right query and returns the data with the associations included. Here too, Extralite can help us, since it can transform projections of joined rows , effectively converting a result set into an object graph:

class PostsStore
  POSTS_WITH_AUTHORS = Extralite::Transform do
    {
      id:       integer.identity, # posts.id
      title:    text,             # posts.title
      body:     text,             # posts.body
      stamp:    integer,          # posts.stamp
      author:   {
        id:     integer.identity, # authors.id
        name:   text              # authors.name
      }]
    }
  end
  
  def all_with_authors
    @db.query POSTS_WITH_AUTHORS, <<~SQL
      select posts.id, posts.title, posts.body, posts.stamp,
             authors.id, authors.name
      from postss
      join authors on authors.id = posts.author_id
      order by posts.stamp desc
    SQL
  end
end

Now, you might say: all this code for something I could’ve gotten for free with ActiveRecord! Yes, it requires some SQL and support code to be written in order to implement this, but from the point of view of your app, the API is just as simple as before, and the data you get out of the store interface contains everything you need, only it’s expressed using plain Ruby hashes:

# here's how a view might look like, using Papercraft:
POSTS_VIEW = ->(posts:) {
  div(id: 'posts') {
    posts.each { |p|
      div(class: 'item') {
        h3 p[:title]
        h4 p[:author][:name]
        markdown snippet(p[:body])
      }
    
  }
}

# Here's how a controller might look like, Using Syntropy:
def call(req)
  posts = @posts_store.all_with_authors
  html = LAYOUT.render(posts:, &POSTS_VIEW)
  req.render_html(html)
end

Frankly, how is this any more difficult than using ActiveRecord? In addition, instead of deep, chained method calls such as Post.where(...).order_by(...) spread all over our app’s codebase, we have created a special-purpose interface custom made for our specific kind of entity (blog posts) that deals with everything we want to do with them, and abstracts them behind regular method calls, no magic involved.

Another detail I’d like to address is the fact the posts store is implemented as a class, when in fact it is used more like a singleton. The reason I’m implementing it as a class is to be able to inject a database connection into the store object. You can do it in many other ways if you wish, depending on your needs. For example you might want to pass in a connection pool instead of a connection, or just implement the interface as a global singleton module. It all basically comes down to the same idea: the model layer as an interface, not as a DSL driving classes of entity objects.

The store abstraction (it’s really just an interface) can also be used to interact with non-entity data, such as time series data, auxiliary data, a key value store, a job queue etc. A store doesn’t even need to correspond to a single table. Since its building blocks are SQL queries, you can access any number of tables, using all available SQL features, in order to read and manipulate the relevant data.

Passing Interfaces Around

One aspect of this design that merits further discussion is the idea of passing interfaces as parameters to method calls. In Ruby, we don’t really talk about interfaces, we talk about discrete objects with which we interact directly. An interface is also an object, but it doesn’t encapsulate data (though it may have some state), it encapsulates functionality. It is in fact, a container of methods.

I’ve been using this “interface pattern” for quite a while. In UringMachine for example, I/O is performed using an interface, an instance of UringMachine or UM for short:

require 'uringmachine'

machine = UM.new
machine.write(UM::STDOUT_FILENO, "hello, world!")
machine.open('foo.txt', UM::O_RDONLY) do |fd|
  buf = +''
  size = machine.read(fd, buf, 8192)
  machine.write(UM::STDOUT_FILENO, buf)
end

In essence, all I/O operations are done through this interface, which means that you need to have a reference to the interface anywhere you do I/O. This design is not unique to UringMachine. Most notably, the Zig programming language now implements I/O as an interface, which is passed around as a parameter (this, in addition to an allocator interface). Go is another example of ubiquitous usage of interfaces.

While this means that you need to pass the interface object around to different parts of your app, you can use various techniques to simplify working with the interface. One way is to use dependency injection. We saw an example of this above, where we inject a database instance into a PostsStore instance. The same can be applied to UringMachine, where we pass the machine instance to an object that abstracts an HTTP connection:

class HTTPConnection
  def initialize(machine, fd, &handler)
    @fd = fd
    @machine = machine
    @handler = handler
  end

  def respond_empty(status = 200)
    @machine.write(@fd, "HTTP/1.1 #{status}\r\nContent-Length: 0\r\n\r\n")
  end
end

A related approach is by using closures, which is especially useful when dealing with callables:

def make_posts_handler(posts_store)
  ->(req) {
    posts = posts_store.all
    req.respond_html(render_posts(posts))
  }
end

app.start(&make_posts_handler(@posts_store))

One important consequence of using interface objects, is that it encourages you to build your app in a more responsible way. For example, you might be tempted, unless you knew better, to read or manipulate posts somewhere in the bowels of a view template. Well, with this kind of design, you can’t, unless the template code has gotten a hold of a PostsStore instance, which is a bad idea and should be verboten . Thus, you can make sure that any part of your code that doesn’t hold a reference to a PostsStore interface can’t touch the database, which should contribute substantially to you keeping more hairs on your head!

Taking Advantage of Prepared Statements

But let’s get back to ORMs. Another missing feature in ORMs is the ability to use prepared statements. Prepared statements, in SQLite and I believe also in PostgreSQL are queries that have been prepared in advance and can be executed again and again by the application, without the database having to parse the query again and again each time it is executed. Prepared statements are ephemeral - they exist only for the duration of the database connection. In SQLite, those are simply normal queries ( statements in SQLite tech lingo) that are kept in memory in order to be reused, instead of being discarded directly after being executed.

Since we’re dealing with (in most cases) a finite number of different queries, why should the database have to parse over and over again the same queries? We can use prepared statements for that. While Extralite does have an Extralite::Query class that implements prepared statements (or queries), I’ve been working recently on automatic caching of statements at the database level, such that any query that’s issued with parameters is stored in a cache, and automatically reused whenever the same SQL is given to Database#query .

The code is not yet released (hopefully it’ll be ready by the end of the month), and I still haven’t done any benchmarking to see how it affects performance. Using this feature you will get slightly higher memory usage (each prepared query consumes a few KBs of RAM), but you’ll reduce CPU time, and you’ll also reduce allocations.

I’m excited to see where this goes, and how far we can push the idea of getting the most out of SQLite databases. Ruby is hella fast nowadays, and it’s only getting faster and better. now it’s our turn to get rid of wasteful and unneeded abstractions, and engage again in writing faster, leaner, better software.

Vancouver strip club's Instagram taken down over sign featuring lake joke

Hacker News
www.vancouverisawesome.com
2026-09-06 00:07:40
Comments...
Original Article

Vancouver's Penthouse Night Club has done it again.

It appears the strip club had its Instagram account taken down not because of nudity, but because of a statement on their sign.

The club's marquee on Seymour Street was updated by Ben Jackson with one of his witticisms, calling out U.S. President Donald J. Trump's "renaming" of Lake Ontario.

Can't handle a post

The sign had a simple message, short enough for anyone to understand.

"Can't handle a pool, but wants our lake?"

The Lake Ontario/Lake America spat is part of Trump's trade negotiations with Canada. Ontario Premier Doug Ford responded with his own sign stating "Lake Ontario: Now and Always."

Jackson's sign also refers to the Washington reflecting pool debacle . For weeks, the pool at the Lincoln Memorial was filled with algae and became a national embarrassment for Trump, and cost millions to fix. Trump blamed " VANDALISM ." One person was charged, but the case was dismissed .

Penthouse owner Danny Filippone and Jackson talked about the sign before it was put up, had some giggles, and then Jackson put it up Friday afternoon (Aug. 28).

Filippone says he knew it was a hit right after it went up, as cars driving down Seymour honked and gave the thumbs up.

Social media post taken down

It seems not everyone is a fan, though.

"Around midnight, it's like all the music stopped," says Filippone. "I looked at the bartenders and asked what's going on. They showed me their phone and said 'Our Instagram has just got wiped out.'"

After putting the sign up, Jackson had posted a photo of the sign to the @ Penthouse47 account with a caption reading "Make Lakes Ontario Again." As of publication, the Penthouse account remains offline.

"It took six hours before someone, we assume on the other side of the border, to think it wasn't funny," says Filippone.

The Penthouse has some experience making sure their usual posts don't cross the line.

"Running a strip club Instagram account is difficult enough," Jackson tells Vancouver Is Awesome. "There are so many things you can get punished for these days, but I don't think a funny sign should be one of them."

Filippone says they're careful not to break any of the rules one might expect a strip club to break.

"If you follow our Instagram, this sign would not be on your bingo card for what would get us taken down," Filippone says.

He also says that the sign doesn't name any names.

"It goes to show you what sort of tensions there are between Canada and the U.S."

Small sign, big hit

The Seymour sign has been a popular sight in Vancouver for a few years now, with several of Jackson's Penthouse marquees going viral.

The club went international last year for its " Forever Neighbours, Never Neighbors " sign, which also took a shot at Trump's attacks on Canada.

That sign also got the Penthouse in hot water with a different social media platform when Twitter/X took down their account.

Filippone says the stated reason was "hate speech." It took the Penthouse around a week to regain control of their account that time. That platform has left the Penthouse alone this time (so far).

The "Can't handle a pool, but wants our lake?" might surpass "Forever Neighbours," Filippone says, as far as popularity . He's getting texts and emails of support.

"They appreciate that there's a strip bar on Seymour Street of all the places fighting back," he says. "It's hard to believe, but it is what it is."

He's again surprised by the role the marquee has taken in Vancouver.

"Once again, it always amazes me how the Penthouse Night Club, that's been there for 80 years, and how a sign on Seymour Street has become a part of Vancouver," he adds.

In the meantime, Jackson is in contact with Meta, trying to regain control of the Penthouse Instagram account.

"I'm sure we will figure it out," Filipone says. "In the meantime, the sign is proudly up there."

Vancouver Is Awesome has reached out to Meta for more information on the take down.

AMD Based FreeBSD Desktop Reloaded

Hacker News
vermaden.wordpress.com
2026-09-05 22:24:57
Comments...
Original Article

My previous AMD Based FreeBSD Desktop is still up and running – it will now be my kids computer … so I needed a new one … but not a bigger one 🙂 I spent way too much time finding new Mini-ITX case for the next one … and after all possibilities … I got Silverstone case again :] … because nothing else was this small and with all needed features. Also … I was thinking WHAT exactly should I buy – especially in the times with AI prices. For one of my older projects that failed (I hope to also publish about that some day) I had AMD Ryzen 4750GE CPU with 35W TDP but still 8C/16T CPU on AMD4 socket … so instead of killing all price/performance efficiency with DDR5 and AM5 setup … I went to decent AM4 option.

Case

Older build used Silverstone SG05 case and this one uses Silverstone SG13 … which is almost the same.

  WHAT     SG05     SG13
 Width    218mm    222mm
Height    175mm    181mm
 Depth    276mm    285mm
Volume    10.5L    10.8L

So one can say that its just a tiny 5% increase in size … but a welcome and needed one.

Difference

The new setup is about 25% faster when it comes to CPU and 40-60% faster in GPU department … depending on the workload. The SSD/NVMe speeds will be the same as its still the same AM4 platform … and RAM would also be similar as its again DDR4 sticks.

Assembly

After having the parts laying around for about a quarter (yes I was busy) my son finally forced me to make that build … and it was a positive push – we sit down in dining room with all needed parts and I started to assembly it with my son being more then curious how it is to actually build a computer.

First CPU into the motherboard … and NVME into the M.2 slot.

Next thermal paste and fan onto the CPU.

To be honest it was easier said then done as the Thermalight AXP-90 X47 comes with install options for both AMD and Intel solutions and it took me a little time to check all needed screws to use and their accessories.

Next – motherboard into the case.

Next RAM or GPU … as GPU was measured to fit within millimeters in that case I started with the GPU.

… and no matter how I tried … no matter which angles I tried to use … GPU would just not fit into the case 😦

I found funny that the GPU name was Challenger and it was REALLY challenging to fit it into the case.

I even decided to name that system challenger after that. I am a big fan of Dodge Challenger cars … I own a Dodge personally … just not a Challenger … yet 🙂

I inspected the GPU from every possible direction and one detail caught my attention …

The plastic cover with the fans was about 1cm wider then the PCB for the card … so I decided to cut it precisely at that place.

This is how the plastic cover looked like after being cut to the PCB length.

Here is how it went with the cutting – something like 2-3 minutes of work.

… and after reattaching the cover to the GPU card.

I went back to trying to fit the GPU into the case … but the cut was not enough … it would still not fit … so I changed strategy. I dissembled the expansion slot bracket from the GPU – out that expansion slot bracket into its place in the case – and tried to fit the card again – so later I can add screws to expansion slot bracket to attach it to GPU again … but I was not able to fit all those again. So next ‘victim’ of cutting was the expansion slot bracket … before on the picture below.

… and after 🙂

Not much was needed – but without that cut – fitting that Challenger into the case was not possible.

After that I was able to finally attach the GPU to the motherboard.

It was more or less like that.

… and after the additional bracket cover.

Next for the installation were RAM sticks and SSD SATA drive – nothing spectacular about that – after also adding the PSU it was more or less complete.

Its really nice that Thermaltake Toughpower Grand 750W PSU has detachable cables … so I could have one cable less in my case :] <sarcasm/>

I generally like to build … everything – that means also building computers … but one thing always pisses me off when I build a PC – the cables from the case buttons to motherboard.

Back in 2006 when I was building a gaming PC for my friend it looked more or less like that below – starting a PC with a screw 🙂

Difference

The difference in CPU power between older AMD Ryzen 7 1700 and AMD Ryzen 7 PRO 4750GE is about 25% percent. When it comes to GPU between 5700XT 8GB and 7700XT 12GB its somewhere between 40-60% percent.

There is almost zero ‘visual’ difference between the two – first the older one.

… and the newer one.

The most important part of this article is sharing that all of these parts – especially GPU – works very well with FreeBSD with drm-kmod and x11-drivers/xlibre-xf86-video-amdgpu XLibre AMD Radeon GPU driver. After I learned how much some Red Hat people wanted to cripple and kill the X11 … I am not going back to Xorg X11 implementation … the XLibre X11 is my home … and do not even get me started on Wayland – the details about that topic are in the 200 MB RAM FreeBSD Desktop article.

Firmware

After install and reboot I started the fwget(8) command so that all needed firmware for FreeBSD will be fetched.

challenger # fwget -v
Trying to match device 0x747e in class video and vendor amd with pci_video_amd
Trying to match device 0x8168 in class network and vendor realtek with pci_network_realtek
Trying to match device 0x24fb in class network and vendor intel with pci_network_intel
Needed firmware packages: 'wifi-firmware-iwlwifi-kmod-7000'                                                    

But as it seams it was not enough as I got these boot errors on every FreeBSD system start.

iwmbtfw: iwmbt_fw_read: open: /usr/local/share/iwmbt-firmware/ibt-hw-37.8.bseq: No such file or directory
iwmbtfw: main: Firmware download failed!                      

… so I start to dig where the problem is. For a start I checked all /etc/rc.d services for iwmbtfw name.

challenger # grep -r iwmbtfw /etc
/etc/devd/iwmbtfw.conf: action "/usr/sbin/iwmbtfw -d $cdev -f /usr/local/share/iwmbt-firmware";

challenger # ls /usr/local/share/iwmbt-firmware
ls: /usr/local/share/iwmbt-firmware: No such file or directory

So the /usr/local/share/iwmbt-firmware seems to be missing but its suppose to be there …

challenger # man iwmbtfw | grep firmware
     Firmware files are available in the comms/iwmbt-firmware port.

OK … lets install the comms/iwmbt-firmware package.

challenger # pkg search -o iwmbt
comms/iwmbt-firmware           Intel Wireless bluetooth adaptor firmwares used by iwmbtfw(8)

challenger # pkg install iwmbt-firmware
Updating FreeBSD-ports repository catalogue...
FreeBSD-ports repository is up to date.
Updating FreeBSD-ports-kmods repository catalogue...
FreeBSD-ports-kmods repository is up to date.
Updating FreeBSD-base repository catalogue...
FreeBSD-base repository is up to date.
All repositories are up to date.
The following 1 package(s) will be affected (of 0 checked):

New packages to be INSTALLED:
        iwmbt-firmware: 20251111 [FreeBSD-ports]

Number of packages to be installed: 1

The process will require 15 MiB more space.
5 MiB to be downloaded.

Proceed with this action? [y/N]: y
[1/1] Fetching iwmbt-firmware-20251111: 100%  4840 KiB 619.5 kB/s    00:08
Checking integrity... done (0 conflicting)
[1/1] Installing iwmbt-firmware-20251111...
[1/1] Extracting iwmbt-firmware-20251111: 100%

After reboot I never saw that error message again – so that is the needed fix.

Hardware

This is the exact hardware I got with the prices I paid for it in EUR currency. Some were bought as new and some as used.

 TYPE  EUR  WHAT                                    
 CASE   55  Silverstone SG13
  PSU   55  Thermaltake Toughpower Grand 750W
 MOBO  100  ASRock B550M-ITX/ac
  CPU  145  AMD Ryzen 7 4750GE 8C/16T 35W TDP
  GPU  340  ASRock ATI Radeon 7700XT 12GB Challenger
PASTE   10  Grizzly Thermal Kryonaut 2g
 COOL   40  Thermalight AXP-90 X47
  FAN    5  Savio BLADEX1 120mm PWM
  SSD   45  ADATA SU650 512GB
 NVME   50  Phison ESO512GYLCT-EP3-2L 512GB
  RAM  150  2 x 16GB Goodram DDR4 3600MHz 1.35V CL18
PRICE  995  TOTAL

Not sure it was worth it but I did it anyway.

In the mean time I also upgraded my older HP E221c FullHD monitor for HP E271i FullHD one … with additional HP soundbar mounted below the screen. To be honest – nothing fancy about that – the monitor was about 70 EUR and about 15 EUR for the soundbar.

XLibre X11

I installed these packages.

challenger # \
  pkg install -y \
    drm-kmod \
    xlibre \
    openbox \
    xterm \
    xinit \
    mesa-demos \
    scrot \
    radeontop \
    screen \
    lsblk \
    beadm \
    smartmontools

Next I started the XLibre X11 as usual.

vermaden@challenger % echo openbox > ~/.xinitrc

vermaden@challenger % xinit -- -dpi 75 -nolisten tcp

… and the X11 XLibre display server started as usual – but after I checked the logs … I was using the software scfb driver … and I was not in the video group – so I added myself there.

challenger # pw groupmod video -m vermaden

Next … I searched for amdgpu driver …

challenger # pkg search -o amdgpu
x11-drivers/xf86-video-amdgpu         X.Org amdgpu display driver
x11-drivers/xlibre-xf86-video-amdgpu  XLibre amdgpu display driver

Its important to install the x11-drivers/xlibre-xf86-video-amdgpu one because the other one will force You to uninstall XLibre and install Xorg.

Next I added the AMD Radeon X11 config.

challenger # cat /usr/local/etc/X11/xorg.conf.d/card.conf 
Section "Device"
  Identifier "Card0"
  Driver "amdgpu"
  Option "TearFree" "true"
EndSection

Nothing fancy but works as desired.

I also checked the /var/log/Xorg.0.log for details and …

[2026-09-05 14:48:22] (II) LoadModule: "amdgpu"
[2026-09-05 14:48:22] (II) Loading /usr/local/lib/xorg/modules/xlibre-25/drivers/amdgpu_drv.so
[2026-09-05 14:48:22] (II) Module amdgpu: vendor="X.Org Foundation"
[2026-09-05 14:48:22]   compiled for 1.25.1.9, module version = 25.1.2
[2026-09-05 14:48:22]   Module class: X.Org Video Driver
[2026-09-05 14:48:22]   ABI class: X.Org Video Driver, version 28.1
[2026-09-05 14:48:22] (II) AMDGPU: Driver for AMD Radeon:
        All GPUs supported by the amdgpu kernel driver

Yep … this is what we wanted.

This is how that GPU looks like in the pciconf(8) output.

challenger # pciconf -lv vgapci0
vgapci0@pci0:3:0:0:     class=0x030000 rev=0xff hdr=0x00 vendor=0x1002 device=0x747e subvendor=0x1849 subdevice=0x5323
    vendor     = 'Advanced Micro Devices, Inc. [AMD/ATI]'
    device     = 'Navi 32 [Radeon RX 7700 XT / 7800 XT]'
    class      = display
    subclass   = VGA

This is how resources usage looks like in X11 XLibre session on FreeBSD.


challenger # top -b -ores
last pid:  5702;  load averages:    0.08,    0.08,    0.02  up 0+00:30:16    14:35:51
34 processes:  1 running, 33 sleeping
CPU:  0.0% user,  0.0% nice,  0.0% system,  0.0% interrupt, 99.9% idle
Mem: 92M Active, 101M Inact, 1512M Wired, 2056K Buf, 29G Free
ARC: 141M Total, 24M MFU, 113M MRU, 388K Anon, 670K Header, 2159K Other
     88M Compressed, 207M Uncompressed, 2.35:1 Ratio
Swap: 4096M Total, 4096M Free

  PID USERNAME    THR PRI NICE   SIZE    RES STATE    C   TIME    WCPU COMMAND
90382 vermaden     16   0    0   319M   136M select   6   0:01   0.00% Xorg
92537 vermaden      2   0    0    50M    22M select  12   0:00   0.00% openbox
 1117 vermaden      1   0    0    26M    14M select  13   0:00   0.00% xterm
77479 root          1   0    0    25M    10M select  14   0:00   0.00% sshd
17882 root          1   0    0    15M  4356K select   9   0:00   0.00% devd
89878 vermaden      1   1    0    16M  3808K wait     5   0:00   0.00% xinit
 2151 vermaden      1   0    0    14M  3500K wait     2   0:00   0.00% sh
 5702 root          1   0    0    15M  3492K CPU9     9   0:00   0.00% top
 1924 root          1   0    0    14M  3280K wait     4   0:00   0.00% login

This is how the main FreeBSD /etc/rc.conf config looks like.

challenger # cat /etc/rc.conf
  hostname="challenger.local"
  ifconfig_re0="inet 10.0.0.5/24 up"
  defaultrouter="10.0.0.1"

  kld_list="${kld_list} amdgpu amdtemp"
  syslogd_flags="-ss"
  sshd_enable="YES"
  powerd_enable="YES"
  dumpdev="AUTO"
  zfs_enable="YES"

Far from fancy – just simple stuff.

Power

Power measurement seems similar to older model – but slightly smaller.

POWER  STATE                                     
  47W  IDLE without amdgpu.ko loaded
  39W  IDLE with amdgpu.ko loaded
  45W  BUSY with 16 x 100% CPU Thread(s) @ 1.4GHz
  80W  BUSY with 16 x 100% CPU Thread(s) @ 3.1GHz

The Turbo frequency for the AMD Ryzen 7 4750GE CPU is at 4.3GHz … but I am not sure its used … because for example a CPU in my laptop has two ‘final’ speeds shown as 1801/15000 1800/15000 and the one with additional 1 it the one with Turbo mode … but nothing like that for that AMD Ryzen 7 4750GE CPU.

Also … there is only C1 state supported … which is sad 😦

challenger # sysctl dev.cpu.0
dev.cpu.0.temperature: 47.0C
dev.cpu.0.cx_method: C1/hlt
dev.cpu.0.cx_usage_counters: 259234
dev.cpu.0.cx_usage: 100.00% last 19092us
dev.cpu.0.cx_lowest: C1
dev.cpu.0.cx_supported: C1/1/0
dev.cpu.0.freq_levels: 3100/3778 1700/1615 1400/1277
dev.cpu.0.freq: 1400
dev.cpu.0.%iommu: 
dev.cpu.0.%parent: acpi0
dev.cpu.0.%pnpinfo: _HID=ACPI0007 _UID=0 _CID=none
dev.cpu.0.%location: handle=\_SB_.PLTF.C000
dev.cpu.0.%driver: cpu
dev.cpu.0.%desc: ACPI CPU

This is how the sensors(8) output looks like on that box.

challenger # sensors

            BATTERY/AC/TIME/FAN/SPEED 
 ------------------------------------ 
               dev.cpu.0.cx_supported: C1/1/0 
                   dev.cpu.0.cx_usage: 100.00% last 10882us
                       dev.cpu.0.freq: 1400 
                hw.acpi.cpu.cx_lowest: C1 
                            powerd(8): running

                  SYSTEM/TEMPERATURES 
 ------------------------------------ 
                dev.cpu.0.temperature: 47.1C
                dev.cpu.1.temperature: 47.1C
                dev.cpu.2.temperature: 47.1C
                dev.cpu.3.temperature: 47.1C
                dev.cpu.4.temperature: 47.1C
                dev.cpu.5.temperature: 47.1C
                dev.cpu.6.temperature: 47.1C
                dev.cpu.7.temperature: 47.1C
                dev.cpu.8.temperature: 47.1C
                dev.cpu.9.temperature: 47.1C
               dev.cpu.10.temperature: 47.1C
               dev.cpu.11.temperature: 47.1C
               dev.cpu.12.temperature: 47.1C
               dev.cpu.13.temperature: 47.1C
               dev.cpu.14.temperature: 47.1C
               dev.cpu.15.temperature: 47.1C

                   DISKS/TEMPERATURES 
 ------------------------------------ 
       smart.ada0.temperature_celsius: 34.0C
              smart.nvme0.temperature: 39.0C

Not great. Not terrible.

PKGBASE Upgrade

As I installed stock 15.1-RELEASE I will now upgrade to latest 15.1-pX release available.

challenger # pkg upgrade -r FreeBSD-base
Updating FreeBSD-base repository catalogue...
FreeBSD-base repository is up to date.
FreeBSD-base is up to date.
Checking for upgrades (53 candidates): 100%
Processing candidates (53 candidates): 100%
The following 52 package(s) will be affected (of 0 checked):

Installed packages to be UPGRADED:
        FreeBSD-audit: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-autofs: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-bhyve: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-bsnmp: 15.1 -> 15.1p1 [FreeBSD-base]
        FreeBSD-clang: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-clang-dev: 15.1 -> 15.1p1 [FreeBSD-base]
        FreeBSD-clibs: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-clibs-dev: 15.1 -> 15.1p3 [FreeBSD-base]
        FreeBSD-clibs-dev-lib32: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-clibs-lib32: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-console-tools: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-ctl: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-jail: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-kernel-generic: 15.1 -> 15.1p3 [FreeBSD-base]
        FreeBSD-kernel-generic-dbg: 15.1 -> 15.1p3 [FreeBSD-base]
        FreeBSD-lib9p: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-lib9p-dev: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-lib9p-dev-lib32: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-lib9p-lib32: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-libevent1: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-libevent1-dev: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-libevent1-dev-lib32: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-libevent1-lib32: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-mtree: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-natd: 15.1 -> 15.1p1 [FreeBSD-base]
        FreeBSD-natd-dev: 15.1 -> 15.1p1 [FreeBSD-base]
        FreeBSD-natd-dev-lib32: 15.1 -> 15.1p1 [FreeBSD-base]
        FreeBSD-natd-lib32: 15.1 -> 15.1p1 [FreeBSD-base]
        FreeBSD-ntp: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-openssl-dev: 15.1 -> 15.1p3 [FreeBSD-base]
        FreeBSD-openssl-dev-lib32: 15.1 -> 15.1p3 [FreeBSD-base]
        FreeBSD-openssl-lib: 15.1 -> 15.1p3 [FreeBSD-base]
        FreeBSD-openssl-lib32: 15.1 -> 15.1p3 [FreeBSD-base]
        FreeBSD-pmc: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-ppp: 15.1 -> 15.1p3 [FreeBSD-base]
        FreeBSD-rescue: 15.1 -> 15.1p3 [FreeBSD-base]
        FreeBSD-runtime: 15.1 -> 15.1p3 [FreeBSD-base]
        FreeBSD-runtime-dev: 15.1 -> 15.1p1 [FreeBSD-base]
        FreeBSD-runtime-dev-lib32: 15.1 -> 15.1p1 [FreeBSD-base]
        FreeBSD-runtime-lib32: 15.1 -> 15.1p1 [FreeBSD-base]
        FreeBSD-src: 15.1 -> 15.1p3 [FreeBSD-base]
        FreeBSD-src-sys: 15.1 -> 15.1p3 [FreeBSD-base]
        FreeBSD-syslogd: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-utilities: 15.1 -> 15.1p2 [FreeBSD-base]
        FreeBSD-utilities-dev: 15.1 -> 15.1p1 [FreeBSD-base]
        FreeBSD-utilities-dev-lib32: 15.1 -> 15.1p1 [FreeBSD-base]
        FreeBSD-utilities-lib32: 15.1 -> 15.1p1 [FreeBSD-base]
        FreeBSD-zfs-dev: 15.1 -> 15.1p1 [FreeBSD-base]
        FreeBSD-zfs-dev-lib32: 15.1 -> 15.1p1 [FreeBSD-base]
        FreeBSD-zfs-lib: 15.1 -> 15.1p1 [FreeBSD-base]
        FreeBSD-zfs-lib32: 15.1 -> 15.1p1 [FreeBSD-base]
        FreeBSD-zoneinfo: 15.1 -> 15.1p2 [FreeBSD-base]

Number of packages to be upgraded: 52

579 MiB to be downloaded.

Proceed with this action? [y/N]: y

… and it all went well. After the upgrade finished I rebooted and everything worked like a charm.

Offtopic

Fast forward half a year later after the build happened in that dining room – my 10 years old son comes to me a month ago and says ‘Hey dad I just wrote this PC Builder game with ChatGPT do You want to play it?’ … and after I checked it I was really shocked … positively. I mean … when I was 10 years old I did not even had my AMIGA 600 … I was playing various games on Pegasus – which is a cheap Polish NES clone … I would get AMIGA 600 when I was more or less like 12 years old (speaking from foggy memories) … and I was still playing games like Sensible World od Soccer or Cannon Fodder instead of creating anything. This is how this (after many iterations with AI) PC Builder game looks like.

Yes – its in Polish – but I believe its just one prompt away from making it English/Polish when needed 🙂

My son also surprised me because one of the times I told him that ‘its not only ChatGPT out there – there are others like Perplexity or Claude or Gemini or … and several times he said ‘Dad I run out of tokens and just copied that I had and put that into other AI and it picked up my work and added the changes I wanted …’ – when I look at my son doing this stuff – being literally a Vibe Coder (still at the beginning of his route but still) … I feel like the schooling system is just one big fucking joke … I mean Mathematics/Physics/English/… are needed and useful … bot others? Optional to say the least … and I felt (and act accordingly) when I was in the school. Its just such a fucking waste of time to cram for some exam and forgot all that shit to get a grade.

What I find funny is that my son never played a game I wrote … and I already played two games of his … I wish every parent to have the same.

EOF

Compression in Full Quiet

Lobsters
pineight.com
2026-09-05 22:13:34
Comments...
Original Article

By on 2024-04-25 in Retrotainment

How does Retrotainment Games ' Full Quiet squeeze a vast open world into an NES cartridge half a megabyte in size? It uses several layers of compression to squash the world as flat as a pancake.

🌎️ + 🗜️ = 🥞️

Mmm... pancakes

Data compression refers to transformations on repetitive data to reduce the storage size while allowing the original data to be recovered, either exactly or approximately. It allows a large adventure to be stored in a program cartridge of modest capacity. The engine of Full Quiet takes advantage of various forms of repetition to store its world in what by 2020s standards is a tiny storage medium.

Text compression: DTE 🔗

Look at the repetition in the following 27-character string of text:

the fat cat sat on the mat 

The 3-character sequence at , including the trailing space, appears four times in the string. We can replace it with a shorter code and store what the code expands to. This definition consists of the abbreviation @ , the expansion at , and a symbol to mark the end of the expansion | . It totals 5 characters: @at | .

Adding the definition and the compressed text produces a 24-byte string:

@at |the f@c@s@on the m@

The sequence the likewise appears more than once. However, if we were to add another definition # that expands to the , that wouldn't save any bytes because we'd still need to store the definition.

@at |#the |#f@c@s@on #m@

Text compression doesn't work well with a single sentence in isolation. It works better with a longer text, where each entry in the dictionary represents something that appears much more often.

Much of the text in Full Quiet is compressed, including radio transmission dialogue, the "subtunnel" text during a dream, occasional dialogue with Pap, and the credits. This uses a codec called DTE, or "digram tree encoding", which has also been called " digram coding ", " dual tile encoding ", or " byte pair encoding " in various sources. DTE turns the 128 most common sequences of 2 characters into a reference to that pair. Because ASCII uses only code units 0 through 127, this leaves the rest of the byte range (128 through 255) for references. The "tree" part comes in when one or both of each pair of characters is itself a reference to another pair earlier in the dictionary.

The dictionary takes 256 bytes to store 128 pairs. The first 37 pairs might be as follows:

128: "E "
129: "TH"
130: "S "
131: "T "
132: "IN"
133: "ER"
134: "OU"
135: "AN"
136: "D "
137: "ON"
138: "TO"
139: "LL"
140: "RE"
141: " ",129  ; that is, " TH"
142: ". "
143: "Y "
144: "ST"
145: "EN"
146: "Y",134  ; that is, "YOU"
147: 132,"G"  ; that is, "ING"
148: "RI"
149: " H"
150: ".."
151: " W"
152: " S"
153: "OW"
154: "I",130  ; that is, "IS "
155: " C"
156: "LA"
157: "OR"
158: "HA"
159: " B"
160: " A"
161: "CH"
162: "IT"
163: 129,128  ; that is, "THE "
164: "'",130  ; that is, "'S "

With this dictionary, the string PULL THE ORANGE RING (20 bytes) can be encoded as PU<139> <163><157><135>G<128>R<147> (11 bytes). (The notation <num> denotes a reference byte.) Replacing each reference with its definition, such as <139> with LL , results in PULL <129><128>ORANGE R<132>G , which in turn expands to PULL THE ORANGE RING .

This method compresses the 21 transmissions, 17 subtunnels, 2 Pap dialogue scripts, and the credits from 12,704 bytes to 7,822 bytes with the dictionary included, a savings of 38 percent. There exist other, stronger methods of text compression. Many of them rely on building a dictionary in RAM, something a personal computer has quite a bit of but the NES does not. We chose DTE because it is relatively quick to decode with a dictionary in ROM.

Indexed color 🔗

Duwago, a common enemy, with the index of each dot making it up

Modern computers usually operate in a 24-bit color space. Each dot has 8 bits for red level, 8 bits for green level, and 8 bits for blue level. Full Quiet has a total of 238 different maps, covering 77,418,496 dots. Each map has daytime, evening, and night. If it stored the exact color of each dot, that would total over 232 megabytes per daypart, or over 696 megabytes in total.

The 52 colors that an NES PPU produces, each with a hexadecimal code from 00 to 3F

The NES PPU can't even generate all those colors. It can generate about 52 different colors. This lets us reduce the colors from 24 bits to 6 bits, or 58 megabytes per daypart, or 174 megabytes in all. For each area, we also store a palette translation table that maps each day color to its corresponding evening and night colors. These tables total 351 bytes, letting us store only the images for the day.

Plateau lookout in day, evening, and night

We use no more than 16 of the 64 colors in each map. Each map has a palette that maps indices 0-15 to PPU color codes 00 to 3F, and this can be packed into 10 bytes per map or 2380 bytes in total. Using a palette lets us store 4 bits per pixel instead of 6, and the daypart palette translation operates on this palette, leaving us with roughly 39 megabytes.

Outside the Swamp camp, there is a bit of attribute clash visible as a chunk of gray. This is because none of the subpalettes for this map include both gray (00) and medium green (1B).

The NES was used with televisions of the 1970s and 1980s, which are not quite as strong at showing fine color detail compared to fine brightness detail. We can choose one universal background color (or "backdrop" color) for each map and then four different subpalettes of three colors, where each set applies to a 16 by 16 dot area. This uses only numbers 0 to 3, two bits per pixel, plus the attribute data used to choose a subpalette for each area. This reduces the map data closer to 19 megabytes, at the cost of occasional visible edges between 16 by 16 dot areas where the tool cannot find a close enough set of three colors. These edges are called "attribute clash." Because over 99 percent of NES games use 16 by 16 dot attribute areas, skilled NES artists have to find ways to disguise clash when they cannot avoid it.

To sum up so far: We're storing 77,418,496 pixels across three dayparts in 19,354,624 bytes for pixels, 75,604 bytes for attributes, 2,380 bytes for palettes, and 351 bytes for daypart palette translation, for a total of 19,432,959 bytes. That's slightly more than 1/12 of our original estimate, though still not small enough for the NES.

Tile graphics 🔗

An image of text with the 28 distinct tiles that make it up. From H. Rackham's translation of M. T. Cicero's De finibus .

The NES is designed to work with graphics divided into "tiles" or "characters", each 8 by 8 pixels in size. The term "character" comes from old video terminals that displayed letters and numbers as a grid of fixed-size glyphs, each 8 by 8 pixels.

Say we find 256 different 8 by 8 pixel tiles for each map. Each tile has 64 pixels and, with two bits per pixel, can be defined in 16 bytes. Then we can store the map as a grid of tile references. Instead of 19 megabytes for pixels, now we have 974,848 bytes for tiles, 1,209,664 bytes for tilemaps, and still 75,604 bytes for attributes.

These four maps in the "Swamp turns" mapset reuse a lot of graphics.

A lot of maps in the game have similar graphics, seeing as they take place in similar-looking parts of the world. There are 80 mapsets, about one per three maps, and all maps in a mapset share the same tiles. If all mapsets used all 256 available tiles, there would be 20,480 tiles. However, most don't, leaving a total of 16,821 tiles, which can be stored in 269,136 bytes.

Like four of Capcom's Mega Man games, Full Quiet uses CHR RAM. This means while loading a map, it can combine tiles from the individual mapset with tiles taken from a pool of common tiles shared by more than one mapset. This lets us combine 9535 shared tiles into a common pool of 3054 tiles, where 1090 have 3 or more colors and 1964 have only 2 colors. Of these 1964 2-color tiles, only 1218 turn out to be fully distinct. The rest are the same as another 2-color tile with the same shape and different color indices, such as 0 and 1 vs. 0 and 2 or 2 and 3. We can store one of these, using only 1 bit per pixel (8 bytes per tile), and keep track of which colors to use in each map that uses the tile.

Now we're left with 7286 tiles unique to a mapset at 16 bytes each (116,576 bytes), 9535 references to the tile pool at 2 bytes each (19,070 bytes), 1090 4-color shared tiles at 16 bytes each (17,440 bytes), and 1218 2-color shared tiles at 8 bytes each (9744 bytes), totaling 162,830 bytes for background tiles. We still have 1,209,664 bytes for tilemaps, and 78,335 bytes for attributes and palette-related data, for a total of 1,450,829 bytes. We'll get back to those after one more trick with the tiles.

Run-length encoding 🔗

Line art illustration of a male figure with no legs, a round bottom, and mittens on which he walks
The first image on which PB8 compression was tested. Sketch by Sara Crickard ( @secricket.bsky.social on Bluesky ) circa 2009. View in color

Run-length encoding is a method of compressing data by combining consecutive repeats of a symbol. Consider the following fragment of a catalog:

Black pens, box of twenty ... $2.10
Blue  pens, box of twenty ... $2.35
Red   pens, box of twenty ... $2.50

It's tedious to rewrite the entire words on each line. It was even more tedious on stone tablets. For this reason, ditto marks were invented prior to 600 BCE.

Black pens, box of twenty ... $2.10
Blue  "     "   "  "      ... $2.35
Red   "     "   "  "      ... $2.50

Or say you have a picture of a chinchilla drawn with red, yellow, green, and blue dots. The background is green, the outline is blue, and the fur is mostly yellow with some red spots. Cut one row of 48 dots from the image:

Portion of Pikachu with one scanline highlighted. Copyright The Pokémon Company.
GGGBBYYY YYYYYYYY BGGGBYYY YRRRYYYY YRRRRYBG GGGGGGGG

If a color is identical to the color to the left, you can replace it with a ditto mark.

G""B"Y"" """""""" BG""BY"" "R""Y""" "R"""YBG """"""""

There are several ways to encode which pixels are literal and which are repeats. PB8, the run-length encoding used in Full Quiet , breaks data into packets of 8 bytes. Each packet is compressed to a a 1-byte header with a 1 bit for repeats and 0 for anything else, followed by those byte values that are not repeats.

01101011 11111111 00110011 10110111 10111000 11111111
G  B Y            BG  BY    R  Y     R   YBG         

The 48 dots have been reduced to 6 packets totaling 19 bytes:

01101011,G,B,Y
11111111
00110011,B,G,B,Y
10110111,R,Y
10111000,R,Y,B,G
11111111

However, this is not quite the format of NES tiles . The tile format is bit-packed, with each byte containing parts of the 8 dots that make up one row. It's as if each 8 by 1 dot sliver of a tile were compared with the sliver immediately above it. In addition, each tile consists of two 8-byte planes: one for bit 0 of all pixels and one PB8 packet for bit 1 of all pixels. This fits PB8 neatly, as many combinations of 2 colors (0 and 1, 2 and 3, 0 and 2, or 1 and 3) compress one plane to near nothing.

Left: Sara Crickard's original sketch. Center: black if the 8 by 1 dot sliver repeats upward and white otherwise. Right: matching slivers replaced with red.

This compression turned out to be quite effective on tilesets given its simplicity. Though shared tiles cannot be compressed without compromising ability to quickly retrieve them from the pool, the 7286 tiles unique to a mapset compress from 116,576 bytes to 81,118 bytes, saving 30.4 percent.

We're done with tiles, I promise. Let's do maps.

3-level metatile breakdown of one block in the Cliffs

Metatiles are made out of tiles, just as tiles are made out of dots. Groups of four tiles, 8 by 8 dots each, are combined into one 16 by 16 dot small block. Groups of four small blocks are combined into one 32 by 32 dot medium block, which is as tall as the protagonist. Attribute data, choosing which part of the palette, is also stored in medium blocks. Groups of four medium blocks are combined into one 64 by 64 dot large block. Unpacking a tilemap is similar to unpacking DTE, except there are always three layers, and each mapset has its own metatile dictionary that applies to its maps.

All 188 medium blocks in the Cliffs.

Just as there are up to 256 tiles per tileset, there are also up to 256 small blocks, up to 256 medium blocks, and up to 256 large blocks. Unlike Sunsoft's Blaster Master , which has the same three-level structure, the art style of Full Quiet aggressively hides the tile grid. This means that we do occasionally have to send a map back to the art department when the compression tool tells us a limit was exceeded.

All 108 large blocks in the Cliffs. View full size

Say we have a map as big as the Cliffs, which are 2048 by 2048 dots. Storing a raw tilemap for the Cliffs would take 65,536 bytes. If we store 256 small block definitions at 4 bytes each, 188 medium block definitions at 5 bytes each, and 108 large block definitions at 4 bytes each, that totals 2396 bytes. Then the map becomes a grid of 32 by 32 large block references, to be stored in 1024 bytes. The map is compressed to 3420 bytes, or 5.2 percent of its original size.

Animation of morphing the door at the boundary between the Crags and the Field. One large block has two states with the door open and closed.

We almost never use all 256 large blocks in a mapset. This gives us room to store variants of large blocks in the table. For example, a particular mapset can incorporate large blocks with a particular passage closed, half-open, or open. Then the map loader can choose one at runtime, or even switch an entire map between morph states 0, 1, or 2. The tool sorts the large blocks based on whether or not they're part of a morphing set. Large blocks below a particular cutoff value morph, and large blocks from that value to the end of the table don't morph.

Animation of scrolling through a map near the boundary between the Crags and the Field. Top: contents of video memory, showing where the camera is pointed and what part of video memory is being updated; bottom: area visible on screen.

Whenever the camera moves, the scrolling routine draws a row or column of graphics to the tilemap as it enters the view. When the screen shakes after a change in morph state, the scrolling routine runs 34 times, once for each visible column of the map. Each drawn row or column usually crosses five large blocks (totaling 32). When it reads each large block index from the map, if it is less than the map's morph cutoff, it adds the current morph state before looking up which medium blocks to use.

The 80 mapsets have 14,657 mt16s (average 183.2) for 58,628 bytes, 8699 mt32s (average 108.7) for 43,495 bytes, and 4650 mt64s (average 58.1) for 18,600 bytes, for a total of 120,723 bytes of metatile definitions. Combine this with 18,565 bytes of top-level map data for a total of 139,288 bytes of tilemap and attribute, saving 89.2 percent. (There are also five pairs of maps that appear completely identical.)

The final tally 🔗

The 81,118 bytes of compressed unique tiles, 19,070 bytes of references to 27,184 bytes of shared tiles total 127,372 bytes. Add 120,723 bytes of metatile definitions, 18,565 bytes of tilemap, and 2731 bytes for palettes, and we're up to 269,391 bytes. This is 51.4 percent of the 524288 byte ROM capacity, an effective 0.0278 bits per pixel, and certainly a far cry from the original 696 megabyte estimate.

Intrepid modders seeking to produce a total conversion may notice that we left out a few minor overheads, such as parallax backgrounds, animated background tiles, and sprite sheets. We need to have some secret sauce.

AI, Tools and Transformation

Hacker News
www.ben-evans.com
2026-09-05 22:12:46
Comments...
Original Article

The typical big American company today has hundreds, and perhaps thousands, of different pieces of software. It has giant ‘big iron’ horizontal systems of record like SAP and Workday, it has hundreds of vertical SaaS applications, and then there are hundreds more workflows, scripts, automations and databases, right down to the 10 meg spreadsheet running a department. Very often, the company doesn’t even know quite how much it has, what’s actually being used, and what it’s paying for. And yet, with all this software, the company is full of boring, repetitive tasks.

It can be very tempting to think that AI will sweep most of this away. There’s an old joke that an engineer is someone who’ll spend an hour building a tool to automate a task that would take 10 minutes. But with AI, now you can make that tool in five minutes, and you don't need to be an engineer, and you don’t need to write code. You can just ask the model to make the tool for you, or, more fundamentally, just do the task for you itself . Instead of having to create those tools one at a time, software might be dynamic, generative, free-form, and spontaneous. Massively more tasks can be automated, with massively less software.

If you’re a tool-builder, and everybody in Silicon Valley is a tool-builder, this is intoxicating. But I think it misunderstands where software comes from and how people use it, and I think it misses how companies change.

First of all, most people are not tool builders, and most people don’t instinctively think about how their job could be done in a different way. If you spend all your time in the Silicon Valley bubble, it can be easy to forget this, because your entire world is about creating tools that change how things are done. But if you’re a really great matrimonial lawyer, you spend all your day thinking about your cases and your clients, not about what great legal discovery software would do; if you’re a really great enterprise salesperson, you spend all your time thinking about your product and your clients and your competitors, not about how great sales enablement software could make you more productive.

Products like Excel try to bridge this problem with on-boarding flows, assistants, and templates - everything you see in ‘File/New’ is a suggestion for what you could do with this. But every one of those templates still became a company, and that’s what I see in things like Claude for X as well - this is helpful, but not the answer.

Narrowly, that means that the task to be automated might be sitting in plain sight but the people with that task don’t see it. This is what leads to the idea of the ‘forward-deployed engineer’ - someone who is a builder, and knows what AI can build, can ‘just’ walk around a law firm or an architecture office and see the opportunities lying on the table that the lawyer or the architect doesn't see. (This is also the experience of a lot of people in tech when they were 15, wandering around an internship or their parents’s office - “um, daddy, did you realise you could just do it like this?”).

The deeper problem is most of what we’ve automated in the last few decades wasn’t obvious, even if you are a tool-builder, and didn’t have an obvious solution either. We can all think of examples of stuff we use every day where our first reaction was “Why would I want that?” Very often, it's not obvious that the problem exists, and very often it's embedded or bundled or hidden inside something else. Equally, even if you can see the problem, or think you can, the right way to fix it often isn’t clear either, and the way to fix it is to redefine it or unbundle it, and working that out is hard. For many successful software companies, there were half a dozen failed attempts that came before and didn't find quite the right approach or the right problem.

None of this is solved by making easier to write code - by making it easier to make tools. The hard part is knowing that you need a tool for this in the first place, and then knowing what the tool should do.

But even once you reach that point, you have to get everybody else to use it, too. Many of the problems, workflows and tasks that we might want to automate touch 50 or 500 people across five different departments, three different systems of record, and four different regulatory regimes. You might have a great idea for doing an accounts payable differently, but you yourself can't change how everybody in the company does it. That has to be a purchase, and a decision, and an 18-month sales process.

Second, all of this means that software is bought or chosen or created on a spectrum from top-down to bottom-up - the company buys SAP and the user makes a spreadsheet - and I think it’s useful to think of this also as a spectrum from institutionalised to improvised.

You have tasks that are easy to do in the dedicated tools you already have, whether it’s SAP, Carta or Rippling. These tasks and workflows have been institutionalized - a bunch of people in those companies and your company have spent a lot of time working out the correct way to do that task, and it’s important that everyone do it the same way with the same tools. But then you have edge cases, exceptions and one-off questions, that are hard or impossible to do in those tools. Your users, bottom-up and creating their own solutions, manage these in a fuzzy, improvised space of freeform substrates like Excel, email, shared folders, Tableau, Powerpoint and CSVs, screenshots, PDFs and conference calls.

But once this task becomes something that you're doing all the time, in the same way every time, and that lots of people are doing, and becomes important and has revenue and risk attached to it, then, at a certain point, the company has to institutionalize it. You need audit, security, maintenance and accountability. You pave the desire path and pay someone to set it in stone. As above, you might not realize that the path is there - you might not realise that you have hundreds of people wasting an hour a day doing this - and it might be hard to work out the right way to fix that, but that process is why the company has hundreds of apps.

We went though a lot of this with the shift to SaaS, which was another order-of-magnitude change in how much software we had, along with a new operating model and a new cycle time, and that killed a lot of incumbents that couldn’t make the jump (the real rationale for the ‘SaaSpocalypse’). It’s a continuous and organic flow of bundling and unbundling. All of those SaaS apps do something that you could do in SAP or Excel or email - Carta is a $4bn company that manages one spreadsheet for your CFO -  and sometimes tasks move back. A few years ago I spoke to a consultant who said that half of their jobs were telling people who used Excel to use a database and the other half were the other way around.

Hence, if you’re PwC and you hire 3-4,000 graduates every year, you use dedicated, ‘institutionalised’ software to manage that. If you’re a small firm and you hire five or ten, you use email, a shared folder and Google Sheets. As that small firm grows, at a certain point it will outgrow that, and maybe move to Notion, or to an SME-focused SaaS HCM. But a small team inside PwC might also be using Google Sheets to track candidates to fill a role because Workday is too inflexible - the unbundling begins again.

Now AI rolls across all that. AI will expand all of the existing apps, and there’ll be many new vertical apps, and Excel, and Tableau, Google Sheets, email and all the other freeform spaces for improvising solutions will gain new capabilities. With that cycle, the chatbot itself is a new freeform space that sits next to Excel and email, taking over tasks from them and from your apps, and also losing tasks to those apps.

Now that small company hiring ten graduates might stick in Google sheets a lot longer because AI makes it more scalable, or you might use it as a data store for Gemini, and you might ask “should we get Claude to make something or move this to Notion?”… and then you see there’s a new SaaS app aimed right at you that solves this plus some other problem you hasn’t thought of. AI doesn’t change the question: it creates new choices and moves the thresholds.

I think you can see all of this in the experience of enterprise AI deployment in the last three years. Every big company gave everyone Copilot (or maybe ChatGPT or Claude) and a small number of people are using this a lot (some of whom actually increased their productivity), while a larger set of people are using it a couple of times a week and a lot of the rest of your company isn't really using it at all. This is partly a change management and a training problem, but it's mostly the same problem that you would have had if you'd given everyone in the company a PC and Lotus 123 in 1983, or an internet connection and a web browser in 1997. How exactly does this map to everybody's tasks and the problems they actually have this week? Yes, you did give everybody a PC and Lotus, but that wasn’t how you transformed the efficiency of your invoice processing. Yes, you gave everybody a web browser, but that wasn't how you rebuilt your supply chain management around the internet, and it certainly wasn't how a retailer managed e-commerce.

Narrowly, the way that companies think about changing those kinds of structural processes is to start doing pilots. You run trials of products (both bought and built internal) that use the new capabilities of AI to automate processes that you couldn't automate before. There’s now all sorts of data around how many of these pilots there are, how many work (roughly half, as is normal - this is why they’re pilots !) and what can go wrong.

But again, this is a very old-fashioned CIO conversation around use cases, lighthouses, pilots, heroes, quick wins and measurable results. Meanwhile, the CEO and the board scratch their heads and say “Wait, but we've got 100s of workflows and we've done five or 10 pilots. That doesn’t seem to scale?” Giving everyone in the company ChatGPT does scale theoretically, except that most people aren't really finding ways to use it.

Going back to a hypothetical bank giving everyone spreadsheets in the 1980s, or a retailer giving everyone a web browser in the 1990s, yes, of course you should do that, and yes, of course, you need to think about training and change management and all the other good stuff that KPMG can tell you about. But that isn’t how you think about transforming the way your company works around a generational new technology.

Stepping back, it seems to me that with each new transformative technology, every company has to ask three kinds of questions. First, how do we buy, build and deploy this? Do we do pilots? Should we take the product that's bundled from Microsoft/Google/Oracle, build something ourselves, pay someone to build something, or buy this new thing from a startup? Second, they have to ask how far this changes their operations. What does it mean? What does email mean for us? What does spreadsheets mean for us? The answer to that might be radically different if you were an insurance company or a law firm. And third, you have to ask whether this creates new challenges to your business’s economics, new competitive pressures, or, perhaps, some kind of existential threat.

You don’t answer those questions by giving everyone Claude for X. Indeed, all of this means lots of new pitches for professional services (which is ironical given how many questions AI poses to their own business models). Do you want to work out how to deploy an LLM-enabled voice analytics tool in your call center? You're probably going to call Accenture. The vendors themselves have always been happy to help, and now the big labs have their own ‘deploycos’ - we used to joke that a ‘machine learning scientist’ is a statistician who lives in San Francisco, so maybe a ‘forward deployed engineer’ is anyone that OpenAI hired from a systems integrator. On the other side, your startup is building a great new tool and you want to go to market quickly? You'll probably call the Big Four. You're frustrated with how hard it is to sell AI software into law firms or accountancy firms. Okay - go start an ‘AI-enabled’ law firm and work out if that can be a key point of leverage (or whether it's like starting a ‘PC-enabled law firm’ in the 1980s). And of course, if you’re the board, and you're trying to work out whether this is some kind of existential threat or a massive revenue opportunity, then you’ll think about calling Bain, BCG and McKinsey (or your friendly neighborhood M&A banker) - this is what they do.

Stepping back from all of this, though, there's also a much simpler way to think about the question. With every new technology, we start by using it for the work we already have, and we just do that more and faster. But then, over time, you make entirely new things. We will use AI to automate broad classes of stuff inside existing workflows and existing companies (although, as I’ve outlined above, that will be enormously more trouble and work than just giving everybody a model). But with every previous platform shift, the stuff that actually mattered was the stuff that wasn't even possible before and that no-one even imagined.

There's No Limit to How Bad Code Can Get

Lobsters
zachkehs.com
2026-09-05 22:05:52
Comments...
Original Article

TL;DR: Metaphors like "a sinking ship" are often used to describe codebases, but are misleading. A business will sink long before code quality reaches a hypothetical floor. Technical debt has no bankruptcy, no clean reset, so metaphors that imply an end provide a false sense of security.

Software is in the domain of the abstract. It is not like a building, or a bridge, that is in the physical realm where you can see and feel the nature of the thing. If you continue to add floors and rooms to a building forever, it will collapse. Software faces no such constraint. The code can always get worse. There can always be a new layer of indirection or a reduction in performance.

1: Boarding a sinking ship

Over a decade ago I had my first encounter with an ugly legacy codebase. I had just joined Amazon as a "Software Development Engineer", fresh out of university, and worked on a team that owned code related to processing orders . On the surface, what our code had to do seemed simple. Processing an order involved writing some things to a database and calling into services owned by other teams, either to ask validity questions or to update bookkeeping on their end. My colleagues and I estimated that a sufficient implementation of this system shouldn't need more than two dozen strong engineers to maintain and evolve. Yet, our organization was hundreds of people, and the system had grown so large and complex that it had become impossible to learn how it all worked.

It was rare to stay longer than a few years in this org, and institutional knowledge had eroded. This resulted in code that was full of "haunted graveyards" . Fear suppressed any (under-rewarded) efforts to simplify existing systems. The business rules for what had to be done for each type of order were decided by people long ago who weren't around anymore. These rules could sometimes be found in a hopelessly out of date file proudly calling itself a 'living document', but often the rules simply were not written anywhere we could find. Tracing behavior yourself wasn't easy either because much of the system lived across team boundaries where the code was not easy to access.

When some obscure process wasn't happening with an order that should have been happening, our pagers would angrily notify us that someone in our tangled web of service dependencies was unhappy. As a result of this feedback mechanism, the system stayed afloat, but remained difficult to change and had abysmal performance . Despite this, new layers were constantly added to support the latest Amazon products and features. This felt unsustainable, and this feeling is what makes me and others reach for a "sinking ship" as a metaphor to describe an organization that doesn't pay down their technical debt.

To their credit, there were always ongoing attempts to fix the architecture, and these usually went as follows: A new manager or senior engineer joins the organization and observes that "things are bad". Leadership at the organization agrees and wants to make things better, but there are no engineers available, so each fix attempt includes adding new engineers and teams .

The re-architectures were always a failure: The system required years of study to properly understand and was constantly changing. It's not politically viable to take so long to design a fix for the system, so naturally everyone attempting to fix things must work with incomplete information. Some of this impatience is from within: If you are trying to design a grand fix for such a prominently painful architecture, you are doing it in part because you want a promotion (and you don't want to wait too long for a promotion).

Each cycle would end with the remains of the new attempt permanently grafted onto our architecture and the leading engineer having departed with their requisite promotion. The increased headcount stays because the migration plans are too painful and unpopular to actually finish. The cycle continued as it had long before I had arrived. The sinking ship seemed to have no end.

2: Where does it end?

About three years after I had left, I was chatting on the phone with a former colleague from that team. He had recently left the company after an impressive 6-year tenure and had witnessed the cycle complete again. We were commiserating, and both of us reached for the sinking ship metaphor to describe the org, despite us having left years apart.

"Where does it end? How does it end?" he asked me, curious to hear my take on what would happen to that org in the future.

The question and metaphor didn't sit right with me, and I realized the question conflated two things. Are we talking about the code, or are we talking about the company?

A business can sink. Bad software is a real drag on the business, but how much that actually matters depends on a lot of factors. For a company with plenty of cash flow like Amazon, they can tolerate some bouts of internal rot here and there before it has any meaningful impact on their bottom line. For another company whose business model is more sensitive to software quality, bad software may be a latent invitation to a competitor to deliver the metaphorical hull breach (and no, LLMs don't change this ).

For the code, the sinking doesn't end. It's an infinitely sinking ship, because there is no limit to how bad code can be. You didn't escape a building that was about to collapse. It is in a constant, neverending state of collapse. There's something wrong with using words that imply there's an end.

Software is in the domain of the abstract. It is not like a building, or a bridge, that is in the physical realm where you can see and feel the nature of the thing. If you continue to add floors and rooms to a building forever, it will collapse. Software faces no such constraint. The code can always get worse. There can always be a new layer of indirection or a reduction in performance.

The pedants will rightfully point out that software can completely fail to function if it gets bad enough. In practice, such breaking changes are quickly reverted. The thousands of changes that came before to make the code worse are not. The software continues to 'work' . Other cases without a single breaking change to revert are where the ballooning costs of the bad software eclipse its benefit, or if development velocity approaches zero because nothing can be shipped without a breakage. In all of these cases, it is the business that dies long before the code hits any hypothetical floor (so don't act like there's a floor!).

3: Technical debt has no bankruptcy

The drag of bad software on the business is a real threat and the reason why good organizations pay attention to code quality. Since there is no abrupt failure threshold associated with software quality, it's often described as "technical debt", which can be a better metaphor (debt can compound forever) but is also imperfect.

Debt has an 'ending' point because bankruptcy is a forced reset, and the equivalent in software is a full rewrite, which is rarely an option .

The closest option that a mega-corp such as Amazon has is what I call a side-channel, where they split off a team that builds a new, completely disconnected system with only the minimal set of features needed for some new use case. Moving forward they then have the option to direct more new use-cases at this simplified, separate system. Importantly, the old system must remain and be maintained (it's not an 'end'), because all the old use-cases still exist, and new organizational-level pain is felt whenever deciding which to use in the future . That's not exactly slate-clearing like we think of a bankruptcy.

The wrong mental models about software lead to bad decisions. If a 'hard reset' escape hatch exists, then punting technical debt doesn't seem so bad. The belief that a rewrite around the corner could fix things results in worse decisions today, because the decision-maker today doesn't understand that there is no escape hatch.

Metaphors like a "collapsing building" or a "sinking ship" are not appropriate for software, yet we can embrace them anyway to emphasize what makes software different. The building is infinitely collapsing. The ship is infinitely sinking. There is no natural constraint that will wake your project manager up and force them to deal with technical debt. Software will only stay high quality if we put in the effort to stop the sinking. Grab a bucket.

GPT-6 Astra on robot arms

Hacker News
openai.robocurve.org
2026-09-05 21:52:45
Comments...
Original Article

September 4, 2026

A follow‑up to our comparison of Claude Fable 5 and Fable 5.1 . We gave OpenAI's GPT‑6 Astra control of the same YAM arms under the same Inspect Robots agent policy, on the same two tasks:

“Pick up the red block from the table and place it inside the bowl.”

“Pick up the round blue puzzle piece by the knob at its center and place it into the matching circular groove in the board.”

On the bowl task Astra placed the block in 19 of 20 trials, against Fable 5.1's 8 of 20 and Fable 5 in 1 of 20, in 2.5 minutes per trial to Fable 5.1's 6.8, at an estimated $0.94 per run to $2.12.

The puzzle task is a different story: Astra completed the insertion 2 times in 20 against Fable 5.1's 2 in 20. It reaches the groove and stalls at the same final step Fable does, at $1.36 per run to $2.18.

Block into bowl: the best completed run of each model (highest stage, then shortest), each played in its own time at the same speed‑up. Timers show real elapsed time with thinking pauses removed.


Astra completes the bowl task far more often, at about half the cost per run

2026-09-04T18:50:02.475437 image/svg+xml Matplotlib v3.11.1, https://matplotlib.org/ $1 $2 $3 0 20 40 60 80 100 completion rate (%) Fable 5 Fable 5.1 GPT-6 Astra 2.4× higher completion rate 2.3× cheaper Block into bowl $1 $2 $3 Fable 5 Fable 5.1 GPT-6 Astra same completion rate 1.6× cheaper Puzzle piece into groove estimated cost per run (USD, list price)

Large dots are condition means; faint dots are individual trials (100 if completed, 0 otherwise) at their own cost.


Scoring

Every trial was scored by a human grader on the highest stage it reached, so a run that fails still records how far it got. The rubric is unchanged from the Fable report.

0 No purposeful approach
1 Made contact with the object
2 Lifted the object clear of the table
3 Positioned it above the deposit point
4 Placed it in its final position

Astra places the block almost every time; on the puzzle it stalls where Fable does

2026-09-04T18:50:02.444943 image/svg+xml Matplotlib v3.11.1, https://matplotlib.org/ Fable 5 (n=20) Fable 5.1 (n=20) GPT-6 Astra (n=20) 2 13 3 2 6 2 2 8 19 Block into bowl 0 20 40 60 80 100 share of trials (%) Fable 5 (n=20) Fable 5.1 (n=20) GPT-6 Astra (n=20) 2 11 2 5 4 4 9 2 3 6 8 2 Puzzle piece into groove 0 no approach 1 contact 2 lifted 3 positioned 4 placed

Share of trials per model reaching each stage; n per row is the number of trials in that cell.


Results


All runs

Every counted trial, 120 in total.


Technical specifications

Embodiment Bimanual I2RT YAM arms, 6-DoF per arm with parallel-jaw grippers
Control Absolute end-effector poses ( move_to ): x, y, z, yaw, pitch, roll and gripper, per arm. The robot's IK converts poses to joint angles.
Observation Three camera views (top, left wrist, right wrist) plus proprioceptive state, each turn
Policy agent policy, medium thinking effort, 20-LLM-call budget, 25% speed cap, default safety guardrails on
Models gpt-6-astra , claude-fable-5 and claude-fable-5-1
Harness Inspect Robots 0.58.0
Trials 20 per model per task; puzzle on rig-4 for all models, bowl on rig-3 for the Fable models and rig-1 for Astra
Token counts Wire-level request and response tokens, not billed tokens; cost at list price, $10 / $50 per million input / output tokens for all three models

Limitations

  • Astra's trials were run two days after the Fable trials, and not interleaved with them. The puzzle comparison is on the same rig; the bowl comparison is not: the Fable bowl trials ran on rig-3, which was unavailable.
  • Grading was operator-judged with the model known, so scores are open to unconscious bias.
  • Costs are list price. Anthropic requests were sent without prompt caching; OpenAI cached about a fifth of Astra's input automatically, which is not discounted here, so Astra's cost is, if anything, overstated.
  • Objects were reset by hand between trials, and all models ran at medium reasoning effort only.

Ask HN: Why don't we bring back old school OkCupid?

Hacker News
news.ycombinator.com
2026-09-05 21:17:48
Comments...
Original Article
Ask HN: Why don't we bring back old school OkCupid?
26 points by firefax 2 hours ago | hide | past | favorite | 34 comments

Trademark issues aside... why do we do this swipe culture crap?

Old school OkCupud was darkly beautiful because it told you your best match was -- who cares if they're a mafia princess or work down at the cloud factory or simply have other suitors ;-)

What is stopping someone, aside from patents, from making a match based dating site to compete with the current market?

help


1. Dating apps/sites are a rich-get-richer market. Most people go where everyone else goes, because everyone else is there.

2. The more work you ask your users to do up front, (e.g. answering a bunch of survey questions, taking high quality photos, paying up front) the fewer highly desirable people you'll have, because they can easily find people wherever they go without doing all that work.

3. Swiping is more fun (more addictive) than answering survey questions. OKCupid switched to swiping when A/B testing showed that swiping retained people better than survey questions.


1. Dating apps/sites are a rich-get-richer market. Most people go where everyone else goes, because everyone else is there.

I get that this is true for social networks in general, like messaging platforms, there's a first mover and network effect.

However for dating apps specifically, there is not truly a network effect, all you need to find is one person, and for monogamous relationships (which incidentally are more common among heterosexual couples, whereas Tinder and its descendants, coming from grindr heritage, tend against) it's even more true, as in the ideal scenario each user finds one other user, there's not a huge amount of network interaction.

I also think that both in the general and specific course, there's a lot of incentives for niches, subcultures, alternative cultures, often times part of a culture is to avoid the monotonizing mainstream tropes and memes.


> for monogamous relationships (which incidentally are more common among heterosexual couples, whereas Tinder and its descendants, coming from grindr heritage, tend against)

Describing everyone who isn't straight as "tending against monogamy" is bigotry. I'm aware that there's a 0.1% PP difference or whatever -- still absurd.


You also have a self-selection effect: users willing to fill out more involved surveys may pair with each other at a higher rate than the overall background.


The amazing thing is that people still cite the old stats they published as that remains the only significant body of insight into the dynamics of human behavior in this realm.


I actually prefer the swiping apps. As a man I’m expected to make the first move and on apps like OkC it’s easy to waste a lot of time and effort messaging people who are not at all interested in me. The swiping is a signal that there is at least some mutual interest there before I put effort in.

But the main problem with the dating apps is the business model. Their job is to help you find love, but if you find love you delete the app and they don’t make any more money. So they’re incentivized to keep you single forever. I don’t believe Hinge at all when they say they want you to delete their app as quickly as possible.


> The swiping is a signal that there is at least some mutual interest there before I put effort in.

I think you vastly overestimate the "interest" a swipe represents. A significant number of people swipe out of boredom and a lot of swipes are mistakes.


I became single in 2013 and cupid was definitely where the cool kids were. But it was a desktop thing. You sat at a computer and engaged. You invested. Tinder and swiping are a phone thing. No real investment.

Funny thing, 12 years later I met my wife on the zombified corpse of cupid. So... go figure.


> Funny thing, 12 years later I met my wife on the zombified corpse of cupid. So... go figure.

Must have been awkward. Or were you in an open marriage at the time?


With the disclaimer that I've never operated in this space, I would imagine that a significant barrier to the creation of new dating sites is that most people who care enough to try to build a good service are spooked by the liability involved (hosting boatloads of personal info, getting entangled legally when things go wrong, keeping creeps at bay, etc). At least from my standpoint as a layman it looks like an absolute minefield compared to a nice boring spreadsheet SaaS or something.


Okcupid still exists and operate in this manner. So do other platforms. The reason why it does not proliferate like it used to has to do with the short attention span of the population, and in general, the attention economy. You can’t have people answer 1100 questions anymore.

You can, however, put pretty people on the app for them to swipe left or right, and that drives people way more than Doing work to get a match. Unfortunate but true. So the people who opt in to this difficult end are not enough to drive the platform anyways. At least, it could never be anything like when You and I used it ages ago, when everyone was down for the cause (and had the attention span for it too)


What if the app asks one question per swipe?

Advanced version: and shows a match with the same answer.


It's not just dating platforms that have changed, it's also the users. Online dating used to be a niche you had to actively seek out, which meant that the users were naturally self-selected, had more interesting (for better or worse) personalities, and were (on average) more genuine.

Online dating today is far too mainstream for those qualities to remain. Any new dating platform that gains enough popularity to be useful will be quickly flooded with less-differentiated "normies" that dilute any quality in the platform's demographic, and the "optimizers" who game the platform at the cost of other users' experience.


The Match monopoly buying you out is one problem. A bigger problem is that dating sites have to have a lot of users before anybody wants to use them. (They especially need women users.) This conundrum is a huge barrier to entry in the space. The temptation to pad your user base with fake accounts, scammers, and sex workers is tremendous. If you do that, you boost engagement for a few months and then you die.

Yet another problem is churn. If people find a life partner they drop off the site, and you constantly have to find new users to replace them. This is one way a dating site is fundamentally different from e.g. Facebook. As a result, some of the less-scrupulous sites purposely make it less likely that you will see people who seem like great matches, or they interfere with your ability to message those people, just to keep you using the site longer.

Jump-starting the initial chicken/egg issue in an ethical way is AFAIK an unsolved problem. In the post-OKC era, Bumble is the only one I know of that semi-succeeded because they made it woman-centric from the start.

Finally (yeah, I've thought about this a lot), the modern mobile app-only approach can be a distinct turnoff for people like me. Mobile apps make it difficult to write much text about yourself, which was something OKC encouraged. Apps also present a safety issue: Apps often require location permissions, which means employees of the site can stalk women. (This has happened.) There's no good reason dating apps should require location permission, but most do for the same reason most apps of any kind do: Location gives the site owner personal info they can sell to advertisers. All that having been said, the vast majority of the market probably prefers mobile apps to web sites.

The whole space seems like a cesspool of bootstrapping problems coupled with extreme incentives for unethical behavior. Sorry if I sound pessimistic but you asked what's stopping this from happening. This is my take.


> What is stopping someone, aside from patents, from making a match based dating site to compete with the current market?

Demand.

Old-school dating was like cocaine and the swiping-oriented apps are crack. It doesn't mean there would be no demand for an old-school OKC clone but it's a niche play and given Match Group's dominance, the capital you'd need to invest to compete would be significant.


Nothing, just The Match Group buying everything as soon as it becomes popular and incentivizing keeping people using the apps over successful long-term matches.


Yeah, I'm surprised no one has spun up a competitor that combines evidence based match questions with the rich profiles okc was famous for -- OKC's only failing was you might find out you have someone you're a big match with who does NOT return that sentiment.

I also think we've lost the ability to do things like man up and ask out the barista knowing full well you're switching coffee shops for at least six months if you read that one wrong... when I was like, 20, I used apps because I didn't feel capable of just... talking to people. The MRA stuff is insane, but there's something to be said for being able to ask for what you want.


That's begging the question!

If The Match Group is a steady supplier of startup exits in this space, that should incentivise more people to start. So we should see more people trying to resurrect old school OKCupid, not fewer.

Though I suspect the answer isn't that people aren't trying, but that these trials don't succeed enough for OP to notice.


I've worked for a dating startup in the past and the main barrier is marketing costs. You need to be able to bootstrap a network extremely fast or people will join and bounce within minutes and doing that across any sort of region costs a lot of money since you basically need to blanket advertise. Trying to do that across multiple regions requires serious money.

You can build it and they might come but they will leave and never come back if they aren't seeing people to match with.


My wife and I met on original okcupid. Not much to say other than the system worked! It’s sad to hear they become tinder-y but I guess it’s hard to compete with the immediacy and dopamine prospects of swiping.


From what I can tell, people ARE starting these kinds of projects. They just don't get popular enough for you to notice.


Match owns Okcupid now doesn't it?

With how easy it is to build apps now, is it really unreasonable to bring the old one back in a simple/cheap/sustainable way, that shields it off of being bought by those nightmare data brokers?


I think it's a good idea but in this space, that's probably a smell. I met my wife there, been married 15 years now. 97% match, we've basically never had a big argument.


An interesting challenge with dating sites is that the “bell curves” of attractiveness versus desired attractiveness between men and women aren’t symmetric.

This leads to difficulties running a dating site because women will only say yes to 9/10 or better men, but men will message anyone with a pulse which in turn inflates female expectations.

Many women only ever join dating sites as an ego boost, with no intention to actually go on dates.

I had the idea that the way this could work is a totally automated match making system akin to Uber where you are penalised for not accepting matches made by the system. You don’t have to get married or anything, but if you don’t both turn up at the same GPS time space coordinates it counts against you.

Similarly, the system should require an up front ~$50 fee from both sexes per date. You don’t get it back if you stand up a date.

Etc…

It would be very hard to bootstrap, but once going it would produce many more long term partnerships than “swipe right” style apps.


That might sound good in theory, but no one is going to sign up for that. Who will want to pay $50 to not meet up with the creep some algorithm has set them up with? Honestly, it's kinda dangerous.


For dudes, you just pay. They boost you and give you a bonus boost when you first start.

The tinder strategy is simple. Sign up, make profile, swipe, stop, come back in a week. Then upgrade with in app purchase, hustle to get 2-3 contacts, and churn the subscription. Repeat if needed with a different app and rotate as needed.

You’ll get a date with this method and the rest is up to you.

Look around, hot girls are with toad looking guys all of the time. You need to manipulate the platform so you are front and center.


>Because people were less cynical in the 2010s and there's no way to bring that back.

We were only one year out of the Bush administration. I got a lot of lectures on third way neoliberalism from women who in the same age bracket who today are all stone cold socialists.


Could it be argued that people were cynical about different things? I think a lot of culture is just people uncritically repeating ideas theyve heard.


They seemed plenty 'normative' to me, in the sense of professing strong opinions on eg 'I would never date a Tory voter'.