NAOMS Devlog

Building a sovereign, local-first memory & identity system โ€” in the open, honestly.

NAOMS engineering update

Week 14

1,648 commits โ€” under a third of the week before it. Everything the week touched is below, the largest threads first and every area at the end.

Commits in the window
1,648
(~1,096 non-merge, 552 merges)
Previous window
5,586
this week was under a third of it
Busiest day
2026-06-23
378
Following days
06-24: 311 ยท 06-27: 249 ยท 06-28: 100
Net lines of code
not quoted
the counting rule is below
Process Dispatch free June 29, 2026ยท25 min readยทmeta

Commits per day โ€” Week 14

Four things changed this week

One per area, and each one is a way in rather than a feature inside. None of them is finished, and the substrate underneath all four is the same governed, local-first daemon it was last week.

1

NAOMS became a window you double-click

The whole memory system, daemon included, now runs inside a single native Mac application. The browser tab and the terminal startup sequence are gone, and the bundle is signed and notarized so macOS vouches for it on sight.

2

The real daemon now runs on an Android phone

Not a remote control pointed at a server: the governed runtime itself runs on the device, in full production governance mode, with the graph pipeline behind it and your memory unlocked by an actual fingerprint rather than a stub.

3

It can speak, and the voice never leaves your machine

Speech is generated by a text-to-speech model running on your own machine and kept warm, so it answers without a cold-start stall. No hosted speech service is called, and no sentence of yours leaves the device to be read aloud.

4

Two people held a conversation through it

Two human beings talked end to end, on their own devices, with nothing in the middle reading along. It is the first such conversation rather than a finished messaging product, and the wider chat work remains in implementation.

A Mac that trusts the app before it opens

The Apps sheet on a phone, over the Photos app. "Current stack" lists what is already open โ€” Photos, Files, Chat โ€” and below it "Add app" offers Flow Funding, Hives, Memories, Jobs, Contacts, Connect, Vault, Calendar and Tasks, each with an add button. Nothing here is a command you type. The capture is cropped at the left edge by the device viewport. Dark theme, real capture.

The feature: installing and opening NAOMS the way you open anything else on a Mac โ€” double-click, and it is running.

Before: open a terminal, start a daemon, point a browser at a local port, and hold the whole arrangement in your head while you use it.

Now: one signed, notarized application carries the daemon inside it, and macOS vouches for it on sight rather than warning you about it. (NAOMS is now an app you double-click)

The app is the on-switch, not a nicer window

A browser tab is a guest. It assumes the daemon is already running somewhere, because someone started it first, and if nobody did, the tab shows a disconnected screen and has no idea how to fix that. Waking the system was never the browser's job, and it never became anyone's.

The desktop application refuses that assumption. Double-clicking it starts the daemon, keeps it running while you work, and takes it down when you quit. That is the difference between a window onto NAOMS and NAOMS you can launch: the first is a viewer, the second is a product.

There is more inside the daemon than the word suggests. It is a script run by a bundled runtime, which loads the compiled cryptographic engine over a bridge to native code โ€” so shipping it means shipping a runtime, a script and a compiled library, and making all three find each other on a stranger's computer.

Taking it down is harder than keeping it up

Keeping the daemon alive is the easy half: if it exits when it should not, the application starts a fresh one, and from your side the system was simply always up. The old arrangement delegated that to an operating-system service. The application now owns it directly.

Shutting down is the half that leaks. The daemon starts helpers of its own, and those have helpers. Kill the daemon first and its children are adopted by the operating system, lose the link back to their parent, and keep running โ€” one of them still holding the network port the next launch needs.

So the order is load-bearing: take one snapshot of the whole family while the links are intact, then kill from the leaves inward. Get it wrong and you leak a process that makes the next launch fail, for a reason nothing on screen can explain.

One daemon, not a desktop edition of it

The application does not start a special version of the daemon. It starts the same one, with the same launch arguments and settings that a non-application install uses, so the supervisor inside the window mirrors the old launcher exactly. Only the thing holding the leash changed.

That discipline is the point rather than a detail. A desktop edition with its own subtle behaviour is a second house behind a second front door, and the bugs that live in it are invisible from the first.

A window that rendered onto nothing

One arrangement inside the application rendered every screen perfectly and let none of them reach the daemon sitting beside them. The interface fell back to a polite disconnected state, and first-run setup could never finish โ€” a failure that looks, to the person holding the mouse, exactly like a product that does not work.

That is the honest description of this whole class of work: it is judged entirely by whether anyone notices it, and the only two available grades are "invisible" and "the application opens onto nothing."

Two ceremonies, not one

Signing and notarizing sound like one chore and are two independent operations, which is why so many builds ship broken. (A Mac that trusts the app before it opens)

A Developer-ID signature is a cryptographic seal. It hashes the bundle's contents and signs that hash with a key only we hold, making one narrow and absolute claim: this build came from us and not one byte has changed since. A corrupted download or a tampered binary voids it.

Notarization is a verdict rather than a seal. The signed build goes to Apple's notary service, which scans it and, on a pass, issues a ticket that gets stapled onto the bundle. A build can be signed and not notarized; a fresh Mac still distrusts it. What a stranger's machine wants is both.

A tool that reported success having sealed nothing

The standard way to seal the application reported success on every pass and left most of what actually runs inside it unsealed. Apple's own service is not fooled, so the rejection arrives from a stranger's machine rather than from the tool that told you it was done.

A tool's exit status is a claim about the tool, not about the artefact. It is the same trap as a green test suite over a stub, and it is the one worth carrying out of this section: whenever a step reports success, ask what it would have said had it done nothing at all.

"It boots fine" can be the build machine talking

The first sealed build installed, launched, and then died on boot on a clean Mac. It had been quietly borrowing a mathematics library from software the build host happened to have installed, so it worked in the one place nobody needed it to.

The bundle now carries its own copies. The part that matters most is not the copying but the check beside it: if anything in the staged bundle still points outside itself, the build does not warn โ€” it does not produce a build at all. The proof is mechanical rather than a promise, which is the only form a self-containment claim can honestly take.

Stapling for a Mac with no network

macOS can check a notarization verdict by asking Apple at open time, which quietly reintroduces a first-launch failure for anyone offline โ€” the exact thing this work set out to kill. So the ticket is fetched at build time and written into the bundle, where it can be validated locally and forever.

Both the application and the disk image carrying it are stapled, because the system assesses the image when it is opened and the application when it is launched. Two moments, two things to vouch for.

What was proven, and the one hop we did not fake

Proof came from the shipped bytes rather than a developer's working copy. The signed application was assembled and the daemon launched from that bundle's own contents, reaching a full healthy boot with the cryptographic engine loaded and the local store open.

The last hop is a person double-clicking the signed application on their own machine, with their own identity, and walking through real first-time setup on screen. That is a human verification, and it is not one we perform on someone else's behalf. The machinery is proven up to that doorstep.

The scope, stated plainly

This is macOS first, and only macOS. The desktop work names all three desktop systems; we drove the Mac path first because notarization is the hardest of the three to get right, and proving the shape once is the expensive part.

Real notarization stays opt-in behind an explicit flag, because it needs Apple credentials and several minutes of round-trip that a local-only build should not pay. Linux and Windows are the road ahead, not this week's claim.

The daemon actually runs on the phone

The feature: NAOMS on an Android phone โ€” your memory, your keys, your governed runtime, on the device in your hand.

Before: a phone could only ever have been a thin window onto a daemon running somewhere else, which is a different product wearing the same name.

Now: the governed daemon itself runs on the phone, in full governance mode, with the graph pipeline behind it and your memory unlocked by a real biometric. (Your whole memory system, in your pocket)

The safety catch nobody had switched on

Inside the daemon sits one piece of state called production mode. With it on, a sensitive operation must be backed by a fresh human authentication and the gate fails closed without one. With it off, that same gate is satisfied by a deliberate convenience so that unattended tests need no fingerprint reader.

The desktop daemon switches it on explicitly at boot. The on-device runtime never did, so the presence gate on the phone was being satisfied by the non-production skip. The application looked governed and was running with the safety catch off.

One line fixed it, and it is the most important line in the release: the mobile daemon now enables production mode unless an explicit test-only flag says otherwise. The only way to turn the gate off is deliberate โ€” not a default, not a silent fallback.

The bar we held this to was never "does it render on a phone." It was whether the thing on the phone is the same governed daemon a laptop runs, in the same production mode, or a softer impostor wearing the same interface. A viewer demos identically and differs at exactly the moments that matter: no network, a compromised server, someone asking who decided this was allowed.

What "unlocked by your fingerprint" has to mean

Plenty of applications show a fingerprint prompt that gates nothing but a boolean. Here the fingerprint gates the key the vault is actually encrypted with, which is never stored in the clear: it is wrapped under a key held in the phone's hardware keystore that cannot be used at all until a strong biometric authorises the specific operation.

That wrapping key is generated inside the hardware with an authentication validity of zero seconds, so every single use demands a fresh authentication. The dedicated secure element is preferred, with the trusted execution environment as fallback when the secure element rejects the key.

Unlocking runs a prompt bound to a cipher, and on success the hardware hands back a cipher authorised for exactly one operation โ€” one unwrap of the vault key. There is no window in which that key sits decryptable without a live fingerprint behind it. A stolen phone is a wrapped blob and a key that will not turn.

The design the silicon refused

The first design used an elliptic-curve key, which is clean on paper and threw "no provider found" on a real handset running Android 15. That transformation is simply not registered in the platform's keystore on real hardware.

So the key strategy was rebuilt around a symmetric key, which is the pattern the hardware actually supports for a biometric-bound unlock. The correct-looking cryptographic design loses to the one the silicon provides, and you find that out on a physical phone rather than in a simulator.

A prompt that silently never appeared

On relaunch the unlock would sometimes hang forever with no prompt, no error and no crash. The platform discards an authentication request raised before the screen is fully resumed, so the request was thrown away mid-relaunch and the call waiting on a result never got one.

The fix defers the prompt until the screen reaches a resumed state. Alongside it runs a discipline now held across the whole bridge: always resolve. Success, cancel or hardware error, the waiting call is resumed, because a callback that can fall through is a screen that can hang.

One further seam sits just past the unlock. The first time a returning user unlocks a cold daemon, the embedded daemon has to bring up its signer before it can do anything governed, and the old path looped on an unauthenticated error instead. Without that fix the daemon could be unlocked and still unable to sign anything.

The approval the daemon could not ask for

The deepest problem was not cryptographic at all โ€” it was about what does not run on a phone. On the desktop, a requested approval is projected into a node carrying the cryptographic challenge the biometric must answer, and the gate reads that challenge to know what it is asking you to sign.

On the phone that projection never happens, because the on-device runtime cannot load packages dynamically and therefore starts with none. No package means no projection, no node, and a gate that looks for a challenge on a node that does not exist. It found nothing and failed closed.

The result deserves stating plainly: the most secure-sounding failure mode made on-device approvals unusable, because the biometric prompt was never even reached. The fix writes that node directly on the mobile runtime, mirroring the desktop shape field for field, so the gate reads an identical node on both โ€” and it is stripped out of the desktop build entirely.

A message delivered to nobody

During first-run setup the interface can reconnect on a brand-new connection, leaving the daemon's outbound channel pointed at the old one. The delivery machinery still counted one live socket and reported the message delivered, while the bytes went to a dead connection and vanished.

The honest count and the real delivery had quietly diverged, which is the worse of the two failures: a silent success teaches you to trust a number that is wrong. Stale connections are now pruned the instant a push fails to land, and both sides of the boundary log which connection was reached.

What this is not yet

This is a foundation, not a finished phone. What landed is the governed daemon running on the device in production mode, the hardware-backed biometric unlock, the on-device approval path, and the graph pipeline behind them.

The phone experience people will actually use is still being built on top of that floor. We are marking the floor because it is the part that had to be true first โ€” you can add screens and shine to a daemon that genuinely governs on your device, and you cannot bolt "the real thing runs here" onto a viewer afterwards.

A voice that is made in the room with you

The week's third thread needs no phone at all. NAOMS can now speak to you out loud, in a warm voice rather than the flat robotic one, set up with a single command. The interesting part is a question almost nobody asks about a talking computer: where does the voice actually get made?

Usually, elsewhere. Your sentence is packed up, sent to a company's servers, turned into audio there and sent back. It works beautifully, which is why nearly everyone does it โ€” and it means the reminder about your appointment, and the name of the person you are meeting, left your machine to be read aloud.

Here the model runs on your own hardware, on its graphics chip or its processor if there is none to spare. No account, no round trip, no sentence of yours touching a stranger's computer. The listening side already worked this way, so the spoken loop is now closed on your side of the wall.

The cost that had to be paid for that is the one a hosted service hides by keeping an expensive machine awake for everybody. A model on your laptop has to be loaded before it can say a word, and that load is about seventeen of the twenty-seven seconds a completely cold sentence used to take. Twenty-seven seconds is not a voice; it is a punishment.

So a small helper keeps the model loaded and waiting, on your machine only, which brings the same sentence back in about ten seconds. Speaking then degrades down three rungs rather than failing: the warm helper, then the cold model, then the plain system voice.

One further thing separates this week's chat milestone from a passing test. A test asserts that a known input produced a known output; a conversation asserts that someone who never read the code typed a thought and trusted the system to carry it to a person they have a real relationship with. Three things had to be true at once for that, and "at once" is the load-bearing word: the message left by the path a real tap takes, travelled encrypted with nobody in the middle, and arrived readable and correctly attributed.

What this means, in plain terms

Five general lessons this week's work paid for, each learned by getting something wrong first. The four doorways that opened share one substrate, and none of them opened a path out to a server.

flowchart TB
  subgraph doorways["Four new doorways this week"]
    mac["Mac app
(double-click window)"] droid["Android app
(on-device daemon)"] voice["Local voice
(warm TTS)"] chat["Chat
(first 2-person talk)"] end mac --> core droid --> core voice --> core chat --> core core["The same governed, local-first daemon
(your keys, your memory, on your device)"] core -. "no path opened out" .-x server(["A server in the middle"]) classDef forbidden stroke-dasharray:5 5,color:#888; class server forbidden;

Trust is spent before the first run, not after it

A Developer-ID signature and a stapled notarization ticket mean macOS has already vouched for the bundle when a person double-clicks it. Without them the system's first statement about the product is a warning, delivered while not continuing is still easy. Whatever greets a person before they can act is your real first sentence.

A success on the machine that built it measures the machine

The first notarized build launched cleanly on the build host and died on a clean Mac, because it borrowed a maths library from a package manager the build host happened to have installed. The bundle now carries its own libraries and refuses to build otherwise. A dependency your environment lends you is one your artifact does not have.

An error can wear a verdict's clothes

Every tool call an agent made came back denied. The entitlement check had not run: its query was blocked by a guard, and the exception was caught and dressed up as a refusal. A gate denying everything looks, from outside, like a strict gate. A check that cannot distinguish "no" from "I could not tell" is not a check.

A first instance is evidence; a vision is not

Two people held one real conversation, peer to peer, with no intermediary โ€” and the same walk produced twenty recorded defects, six of them fixed and fourteen still open. The unified, multi-network version is designed and unbuilt. Saying which of the two you have costs the headline nothing it was entitled to.

A hosted convenience puts a hole in the promise it serves

The voice could have been a hosted call, shipped sooner and sounding just as good. It is local instead, because the moment your words leave the device to become sound, "stays on your machine" has an exception in it. A promise with one exception is a default, not a promise.

How much healthier is it than a week ago?

Metric Previous window This window ฮ” What it counts
Commits 5,586 1,648 โ–ผ โˆ’3,938 Every commit in the window, 06-22 โ†’ 06-28
Of those, merges 1,919 552 โ–ผ โˆ’1,367 Merge commits
Of those, non-merge 3,667 ~1,096 โ–ผ ~โˆ’2,571 Everything else
Busiest day 1,845 378 โ–ผ โˆ’1,467 Peak day; 06-24 fell to 311, 06-27 to 249, 06-28 to 100
Spoken sentence, cold โ€” ~27s โ€” One sentence with the speech model asleep; the model load alone is ~17s of it
Spoken sentence, warm ~27s ~10s โ–ผ ~โˆ’17s The same sentence with the model already loaded, measured on the developer's laptop

The counting rule is unchanged from last week: every commit in a seven-day window, split into merges and everything else. A merge count is activity, not feature count. The two latency figures are single measurements on one laptop, quoted because they are the numbers the design decision turned on.

The two speech figures are worth reading as a design record rather than as a benchmark. They are what made keeping the model loaded the whole architecture instead of an optimisation, and the gap between them โ€” roughly seventeen seconds โ€” is the model waking up, which a hosted service hides by keeping a machine awake at somebody else's expense.

Net lines of code are not quoted, and the reason is the counting rule. At this volume, and with packaging artifacts in the mix, the number is dominated by bundled dependencies and generated files, so it would report the size of what we packaged rather than the amount of work.

In one line

the most user-visible week the project has had was also one of its smallest by commit count, because distribution and embodiment are low-churn, high-consequence work.

Five honest notes

  1. The desktop application is macOS only. Linux and Windows bundles do not exist. The Mac path went first because notarization is the hardest of the three, and notarization stays opt-in behind an explicit flag rather than running on every local build.

  2. The Android build is a floor, not a phone experience. The governed daemon runs on the device with a real biometric unlock and the graph pipeline behind it. The surface a person would actually use is still being built on top of that floor.

  3. Chat is one conversation, not a messaging product. Twenty defects were recorded when a real person walked the surface; six are fixed and are not yet in a shipped build, and fourteen remain open. The bridged, multi-network version is unbuilt.

  4. The warm voice is not the same thing as a fast voice. A short phrase came back in about six seconds, and a seventy-four-character sentence sometimes did not finish inside ninety, with the model fully loaded. That was found after the feature was celebrated, not before.

  5. The repaired agent interface reaches new daemons first. The corrected vocabulary is part of the definitions a daemon receives when it is created, so a daemon that has been running for months keeps the old menu until it is re-created or re-seeded.

What changed, area by area

Every area that moved this window. Per-area file counts are not available for this window under a rule we can reproduce, so these are named without a numeral rather than given an invented one.

Five of the six areas below shipped something a person can see, and the sixth is a repair to the interface the other five stand on. That ordering is the week in one sentence: the doors were built, and the hinge they all hang from was tightened in the same seven days.

The desktop application.

The whole memory system, daemon included, now runs inside a single native Mac window, so the browser tab and the terminal startup are gone.

The application starts the daemon, restarts it if it dies unexpectedly, and takes down the entire process family from the leaves inward when you quit, so no orphan is left holding the port the next launch needs.

It starts the same daemon a non-application install starts, byte for byte, rather than a desktop edition with its own behaviour. ( NAOMS is now an app you double-click .)

Signing and distribution.

The disk image and the application both carry a Developer-ID signature and Apple notarization, so macOS vouches for the bundle on sight instead of warning about an unidentified developer, and the signature proves the bits were not altered in transit.

Every nested binary is now signed explicitly, inside-out, because the tool that promises to walk the bundle skips everything filed as a resource โ€” which is where the whole runtime lives.

The ticket is stapled into both artifacts so a machine with no network can still validate it, and a fail-closed check refuses to produce a build at all if any absolute non-system library path survives in the bundle. ( A Mac that trusts the app before it opens .)

The phone.

The governed daemon itself now runs on an Android phone, in production governance mode, with memory unlocked by a genuine fingerprint rather than a stubbed approval โ€” the vault key wrapped under a hardware key that authorises exactly one unwrap per authentication.

Three walls fell on the way: an elliptic-curve key design the hardware refused outright, an authentication prompt discarded when raised before the screen finished resuming, and an approval node no on-device package existed to write, which made the gate fail closed before the fingerprint was ever requested.

A delivery path that reported success into a dead connection now prunes it. ( Your whole memory system, in your pocket .)

The interface every agent runs on.

Four frayed seams were closed in the open, published protocol that carries every action an AI agent takes inside the system โ€” starting a session, checking a permission, calling a tool.

Using a standard wire rather than a private one is a deliberate choice with a standing bill attached: it is the single route every agent action travels, so it has to keep working on the merits, every day. Four things ride it, and each is worth naming.

Every attempt to run a tool passes an entitlement check that asks whether this caller actually holds that capability โ€” merely listing the tools does not, because naming a tool is not using one.

An approval is classified by how much interruption it deserves, so deep trust earns a quiet banner instead of a dialog across your work. Consent produces a signed receipt proving the owner genuinely said yes, and the exchange will not complete without one.

And a starting agent is handed a vocabulary of named operations, every name on it a promise that something answers to it.

What had rotted: the entitlement gate had learned to always say no, because its own internal failure was being caught and rendered as a flat denial, so entitlement was never evaluated at all โ€” a gate denying everything looks, from outside, like a strict gate.

An approval's trust tier was announced live but never written down, so a reconnect read the record, found nothing, defaulted lower, and turned that quiet banner into a dialog.

Approving on any surface other than one particular popup left the agent waiting forever, because only that widget minted the receipt โ€” the daemon now mints and delivers it whichever surface you pressed, because which button a human used is cosmetic and the proof is load-bearing.

And the vocabulary handed to a booting agent named a procedure that had been renamed, so the menu advertised something that no longer answered. ( The governance socket is back .)

Voice.

NAOMS can speak aloud in a warm, natural voice, generated by a text-to-speech model running on your own machine โ€” on its graphics chip, or its processor if there is none to spare โ€” and installed by one command.

A small helper keeps that model loaded, which is the whole trick: the load alone costs about seventeen seconds, a cold sentence about twenty-seven, and a warm one about ten. Speaking degrades down three rungs rather than failing: warm helper, then cold model, then the plain system voice.

The spoken audio carries a courteous honorific that the written log deliberately does not, so the record keeps the literal message. ( Giving the system a voice that stays home .)

Chat.

Two people held a real conversation through NAOMS chat, end to end, on their own devices, with no server in the middle reading along.

The sentence was signed and appended to the sender's own record before it was anyone's job to deliver, encrypted to the recipient's own identifier rather than to an account on a host, and rendered readable and correctly attributed on the far device โ€” so the plaintext existed exactly twice and never in between.

A relay moves an opaque blob, and a dark device is not a failure: the message waits in an outbox until that device is reachable. A real-person walk of the surface produced twenty recorded defects, six fixed and fourteen open. ( The first two people talked .)

Named, not claimed

Linux and Windows desktop bundles

NOT CLAIMED โ€” the Mac path went first and stands alone.

Bridged messengers unified in one place

NOT CLAIMED โ€” one peer-to-peer conversation is what exists; the unified list is designed and mocked.

An external audit of the macOS signing and notarization

NOT CLAIMED โ€” Apple's notary service is not an audit of our packaging.

A phone surface above the daemon

NOT CLAIMED โ€” the floor runs; the rooms on top of it are unbuilt.

โ† more in Process   home โœฆ   all โ†’