NAOMS Devlog

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

NAOMS engineering update

Week 7

A week of access and control work across the codebase. Everything it touched is below, the largest threads first, then what it means in general terms, then every area at the end.

๐Ÿ“… Apr 27 โ€“ May 3 ยท Process
Process Dispatch free May 3, 2026ยท23 min readยทmeta

Commits per day โ€” Week 7

Four things changed this week

Two landings, one architecture set down in writing, and one evening that turned a status report into a standing rule.

1

Access is now decided one item at a time

Sharing grants access to one specific thing rather than everything at once, with pending approvals visible while they wait and a signed record you can audit later.

2

An AI can work the real controls, not a weaker copy

A capable model starts a governed session, reads the procedure and advances a workflow through the same controls a person uses, with its own identity on every action.

3

A status report was sent back for want of its evidence

Calling was reported as in good shape. The hundred-plus screenshots agreed on as proof did not exist, and the bar was written down instead of lowered.

4

Large files were given a transfer path of their own

Bulk data must not travel the gossip path that carries small announcements. That is now a stated rule rather than an unwritten habit.

Granting access to one thing, not to everything

The Grant Consent form under Privacy & Consent. A grant names who it is for, one capability (Read), and a specific domain or resource โ€” the hint suggests "sharing.location" or "memory" โ€” with an expiry in days, a maximum number of uses, and a checkbox for whether the person you granted it to may pass it on. Access is a decision about one thing at a time, not a switch. Dark theme, real capture.

The feature: letting someone else read something of yours โ€” a conversation, a file, a slice of your history.

Before: access was a single decision covering everything in scope. Now: it is a decision per item. You grant the one thing, the requests still waiting are visible while they wait, and what you granted stays on a record you can read back later. (One authoritative answer to "who consented to what about me?")

A permission whose object you cannot name is not really a permission. The question a person is actually answering is not "do you trust this peer" but "may they see this". An all-or-nothing grant forces the second question to be answered as the first, and the honest answer to the first is almost always no โ€” so the safe setting becomes the useless one, and people work around it.

The subject, not the actor, is what the record hangs on

Most permission systems organise around the actor: can this person do this thing? Consent asks a different question โ€” whose data is this, and whose permission is the one that matters. The two look similar and store completely differently.

Organising by subject means "who has consented to what about me" has one authoritative answer in one place: your own record. The alternative is reconstructing it by walking every actor's permissions and inferring, which is an answer you cannot check.

What was built. Three parts had to arrive together for the grant to mean anything: the grant itself, scoped to one subject; a queue of pending requests that is visible rather than silent; and a trail that survives the decision, so "who did I let in, and when" is answerable a month later rather than reconstructed from memory.

The pending queue is the part that is easy to leave out. A request that is neither granted nor refused is a real state, and a system that hides it leaves the asker unable to tell patience from rejection. Making the wait visible costs one screen and removes an entire class of misunderstanding.

One door, because a rule enforced in three places is not enforced

Consent used to be written from more than one place in the code. A grant here, a revocation there, each with its own path โ€” and every path is somewhere the rules can drift apart without anyone noticing.

That drift has a specific shape. One writer signs the event and another forgets; one records the subject correctly and another approximates it; one checks that the change is legal and another does not. What consents exist then depends on which code path happened to run.

Those writers were collapsed into one. There is now a single place a consent event is written and a single way to write one, which is also the single place where everything that must be true about a consent change can be enforced.

Signing, validation and routing now have exactly one home each. Every consent event is signed, so it is attributable and tamper-evident. Every state change is checked, so a consent cannot be revoked before it was granted or revived after it was killed. Every event lands against the subject it concerns. None of that is guaranteed by a rule written in one of three writers.

A grant is not binary forever. Permission gets given, paused, resumed and withdrawn, and a system that models only on and off ends up in states it cannot reason about.

All of those transitions now travel the same single path: granting, suspending, reactivating, revoking, and defining the tier that says how much was agreed to. A suspended consent is paused with its history intact rather than destroyed, and can be brought back.

Because each transition is a signed event on the subject's own record, consent is not just a current value but an auditable history. You can ask when it was granted, whether it was ever suspended, who withdrew it and when โ€” each answer a tamper-evident fact rather than a log line you have to trust.

What it cost. Per-item grants multiply the number of decisions a person makes, and that is the honest trade. The answer is not to collapse them back into one switch but to make each decision small, legible and reversible โ€” the record exists so that a grant made quickly can still be reviewed slowly.

The trail matters most at the moment nobody is around to explain. Access granted six weeks ago, to a person whose reason for asking has been forgotten, is exactly the case where memory is worthless and a written record is the only thing that answers.

Why this was the week's clean landing

The rest of the week was the messy middle of several large things. This is the one piece that reached sign-off with no caveat attached, and the reason is worth separating from the feature itself.

The design mostly removed ambiguity rather than adding capability. Many places that could write consent became one; scattered permission checks became a signed record per subject with an explicit set of legal transitions.

Ambiguity is what makes work drag. Every ambiguous path is somewhere a defect can sit undisturbed until the day you claim the thing is finished, which is the worst possible day to meet it. Take the ambiguity out first and the clean landing arrives almost as a side effect.

An AI operates the machine, rather than describing it

The feature: letting a capable model actually run the work โ€” start a governed session, look up the procedure it is meant to follow, advance a workflow step by step, and hand pieces of it to smaller models.

Before: the model sat outside the system. You pasted context into it, it handed you text, and you performed every action yourself. Now: the service that runs NAOMS on your machine exposes its real governance, session and workflow controls over a protocol models already speak, and the model calls them like any other caller. (Don't give an AI a weaker copy of your controls)

Giving a model a separate, weaker copy of your controls moves the audit gap into the human. If the model can only produce text and a person translates every suggestion into an action by hand, the translation is the step nothing records. Calling the real controls means every action the model takes runs the same checks and lands in the same signed record as a person's.

What driving actually looks like

Booting a session is the first call and it is not a formality โ€” it establishes the governed context everything after it runs inside. Policy is checked before anything changes, rather than after something has already been written.

The model then pulls the canonical text of whatever procedure governs the task, so it follows the real steps rather than a half-remembered version, and advances the workflow one step at a time with each step validated against what it was supposed to produce.

The boundary does the translating, once

The tooling on the model's side sends every parameter as text, and the handlers on this side expect properly typed values. Something has to reconcile that, and where it happens decides how failures read.

The conversion happens once, at the door, rather than being re-attempted inside every handler. A value that cannot be made sense of fails at the boundary, not three layers deep with a confusing message about a field nobody asked about.

A dozen sessions, a dozen identities, one machine

In our own development there is not one model session talking to one service. There are roughly a dozen working the same code at once, and sharing naively would smear them into a single actor in the record.

Each session carries its own signing identity with every request. Two sessions hitting the same service do not blur together: who did what stays legible even when twelve of them are working at the same moment.

There is an unglamorous defensive choice alongside it. The controls are presented in coarser groups by default, specifically to avoid a caching fault on the model's side where its view of the available controls could go stale. That kind of decision only surfaces after weeks of many sessions against one service.

Most of the work goes to a small model on your own machine

The capable model has a job title, and it is not "does everything". It plans, holds the shape of the task, and parcels work out. Small models running locally take a bounded piece of work and execute it. (Plan with the big model, work with the local one)

That preference is encoded rather than hoped for. Work dispatches to local runners by default, and a cloud-backed path is taken only when local genuinely cannot meet the task. The word only is doing the work in that sentence.

Three reasons reinforce each other, and the first is not economics. A system where every sub-task phones a vendor stops working when the network does, which contradicts the commitment that it be complete on its own. Cloud inference also costs money per call on a bursty workload โ€” the pool is sized at roughly five to ten queries an hour, with heat rather than money as the binding constraint on a phone. And work done locally is work whose contents never left your machine.

Local workers are not unsupervised. Each runs in its own isolated workspace and its own process, with a bounded budget on how deep and how long it may delegate, and its output is scored โ€” approve, retry, or skip โ€” before it is accepted.

One door for every engine you can run

A laptop can run a surprising number of inference engines and none of them agree on anything. One talks to a background service over the network, one loads weights straight into the process, one exists only on Apple Silicon, one wraps a command-line transcription tool, and a cloud vendor wants a key and a round trip.

Every one of those registers behind a single abstraction. New engines add an implementation, never a new way to call models โ€” so the door does not move when the crowd behind it changes. (One door for every model you run)

Callers ask for a purpose, not for a named engine. A feature says it needs a chat answer, or coding, or transcription, or vision โ€” and a router decides which backend serves it, from what is installed, what the device can take, and what is free right now. The same line of feature code then does the right thing on a phone, a laptop and a workstation with a real graphics card.

Device capability is an input to that decision rather than an afterthought. On a phone the binding question is not whether a model can run but whether running it will cook the device: roughly 44% throttling has been observed on an iPhone under sustained load, and the router treats that as back-pressure.

Startup registers every engine and loads no weights at all. The only thing a provider does at boot is ask the filesystem whether its model files exist; the expensive load waits for the first real request. A person who never touches the vision path never pays to load a vision model.

What is still missing, named rather than papered over. An older and more ambitious plan โ€” distributing model weights peer-to-peer as first-class packages โ€” did not ship. Model management still leans on a local command-line tool and files on disk.

Why hand an outside model the real controls at all

Exposing a governance surface to a model built by someone else sounds like the risky option, and it is fair to end on that. The answer is that the alternative is worse in a way that is easy to miss.

If the model can only produce text, a person has to turn every suggestion into an action by hand. The person becomes the bottleneck, and the record acquires a gap in exactly the place the real work happened โ€” the translation.

Calling the real controls closes that gap. Every action the model takes passes the same policy checks, lands in the same signed record, and answers to the same rules as a person's action. The model gets reach; the system keeps control.

The boundary between the planner and the workers is also settled by preference rather than by evidence. Local models are not yet as strong at planning, so the rule resolves the question by fiat; what happens when a local model matches the cloud one at planning is written down as an open question, not answered.

What this means, in plain terms

Most of what lands in a week belongs to the thing it landed in. A few things generalise past it, and this week's are unusually consistent about one theme: the difference between a claim and something you can check.

Five general lessons this week paid for, each learned by getting something wrong first.

A pipe is chosen by the shape of the payload, not by convenience

Moving a large file by gossip makes every peer re-relay every chunk to every neighbour until the mesh saturates. The same duplication delivers a small control message through flaky links. Neither transport is better; they answer different questions. A mechanism's best property and its worst are usually the same property, seen at a different scale.

Name the object, or the permission means nothing

Access used to be one decision covering everything in scope; it is now a decision about one specific item, with the waiting requests visible and the outcome auditable. The narrower question is the one a person can actually answer. A control that cannot be pointed at a subject will be set to "no" and then worked around.

A result you cannot open is a sentence, not a fact

Calling was reported as in good shape against an agreed bar of a hundred-plus screenshots from a real end-to-end run. The screenshots did not exist; a true statement about a small part had been presented as a claim about the whole. Distrust any report that describes a result instead of showing one you can open.

Let callers say what they need, not who should provide it

A dozen inference engines each speak their own dialect, and letting every feature pick one directly turns each engine into a tendril reaching into every caller. Features now name a purpose and a router picks the backend. Put the choice of provider behind the statement of need, and adding a provider stops touching everyone.

A feature needing a different data structure is a different feature

One-to-one chat and secure group chat both look like "chat", but adding a third person introduces a re-keying problem the two-party case never has. Group messaging was given its own line of work rather than grown inside the chat that existed. Name the different problem early, or you will tear it out of the wrong place later.

How much healthier is it than a week ago?

This week is counted in landings rather than in volume. Two pieces of work reached a verified state, two were held short of one by a bar they have not met, and one defect was found.

No figure for lines of code appears here. At this stage the diff is dominated by fixtures and generated material, so a net line count would mislead rather than inform.

Two honest notes

  1. The offline-sync work is verified, not finished. It cleared its checks on 3 May, and a follow-on fix that same night proved it on the path the product actually uses. The last review was still running as the week closed, which is why this says "verified" and not "done".

  2. Calling and calendar verification did not ship. Calling has real progress and no cross-machine call to show for it. Calendar verification drew heavy commit volume and real screenshots this week and is still work in progress only. Both are named here rather than rounded up.

In one line

access became a decision about one specific thing, a capable model gained the real controls instead of a copy of them, two devices that drifted apart now reconcile, large files were given their own transport by rule, and a model-download offer went missing quietly enough that nothing noticed.

What changed, area by area

Every area that moved this week, with what moved in it. Per-area file counts are not available for this window; the areas are listed by what landed rather than ranked by traffic.

Sharing and consent.

Access is granted per item rather than all at once, signed off and shipped on 30 April.

Pending approvals are visible while they wait, duplicate writers were collapsed into one write path, and the full lifecycle โ€” grant, suspend, reactivate, revoke, and the tier that scopes it โ€” travels that same path as signed events on the subject's own record.

A suspended consent keeps its history rather than being destroyed, so the record answers when it was granted, whether it was ever paused, and who withdrew it, each step tamper-evident rather than a log line you have to trust.

Model operation over a standard protocol.

The service that runs NAOMS now exposes its real governance, session and workflow controls to a capable model over a protocol that model already speaks.

Booting a session, checking policy before anything changes, pulling the canonical text of a procedure, advancing a workflow a step at a time and dispatching a sub-task are each a call over that protocol, with the service deciding what every call means and whether it is allowed.

Text parameters are converted to typed values once at the boundary so a nonsense value fails at the door, and a single service handles many concurrent model sessions with each carrying its own signing identity into the record.

Delegation to local models.

Planning and doing were split into two roles, with the capable model planning and small local models doing most of the labour.

Dispatch goes local first by rule and reaches for a cloud-backed path only when local genuinely cannot meet the task, which is what keeps the workforce alive with the network unplugged.

Each worker runs in its own isolated workspace and its own process under a bounded budget on how deep and how long it may delegate, and its output is scored approve, retry or skip before being accepted โ€” so the cheap path never quietly becomes the unaccountable one.

Inference routing.

Every engine โ€” a background-service runner with retry and a circuit breaker, an in-process loader, an Apple-Silicon runner that registers only where it can run, a speech-to-text wrapper around a command-line tool, text-to-speech paths including the operating system's own voice, a high-throughput server runner for a machine with a real graphics card, a remote-fleet runner that borrows another device's models, the cloud vendors as self-contained packages, and a mock for tests โ€” now registers behind one abstraction.

A router picks by purpose and device capability, treating thermal headroom as back-pressure rather than something to ignore, and startup registers everything while loading no weights at all โ€” a provider's only job at boot is to ask the filesystem whether its model files exist.

The shape it replaces is the obvious one, where each feature talks to whichever engine it prefers: faster to write on the first day and unmaintainable by the thirtieth, because every engine becomes a tendril reaching into every caller and supporting a phone means special-casing heat in a dozen places.

Adding the graphics-card server runner later changed no feature code at all; it simply started being chosen on the machine that had one.

Large-file transfer.

Bulk data is now explicitly barred from the gossip path and belongs on a direct, content-addressed transfer.

Gossip is epidemic broadcast: each peer tells a few neighbours, who tell a few more, until a small fact has reached everyone with overwhelming probability. That redundancy is free for a two-hundred-byte announcement and ruinous for a megabyte of video, which then crosses the mesh several times over.

Three things go wrong at once โ€” the payload is multiplied by the fan-out, the fan-out itself is wrong for a transfer that concerns exactly two parties, and the flood drowns the small control messages sharing the pipe, so the transport whose job was keeping everyone coordinated can no longer deliver the coordination.

That third one is why this is a rule and not a tuning knob: you do not merely get a slow transfer, you get a slow transfer and a mesh that has lost its nervous system, and no larger pipe fixes it because gossip is doing exactly what it is supposed to do to a payload that should never have been handed to it.

The rule is stated as a size invariant โ€” anything whose size scales with what a person made does not travel by gossip โ€” and the split is that gossip carries the pointer while a direct pipe carries the bytes, once, verified by hash on arrival, from any peer that happens to hold them.

Live call media is the sharpest case: gossip may announce that a call is being offered and must not carry a single frame. ( Why gossip must never move your big files )

Chat.

Durable one-to-one chat between friends was live this week, with read indicators and messages that survive a peer being offline and reconcile when it returns.

Secure group messaging was carved out as its own line of work rather than grown inside it, because a third participant turns cheap two-party key rotation into a re-keying problem with its own data structure.

Encrypting once per member and re-establishing keys with everyone on each join or departure works for three people and collapses for a hundred; a tree of keys turns the cost of a membership change from proportional to the group into proportional to its logarithm, which for a thousand members is the difference between a thousand operations and about ten.

The direction is named honestly as a direction โ€” the machinery was not built this week. ( Why secure group chat deserves its own home )

Identity at rest.

Your identity is now genuinely inert while you are away: encrypted on disk, with no usable signing key held by the running program at all.

A hostile program that shares your files but not your live secrets can read the encrypted vault and gets ciphertext, because the thing that turns it into a key is proof of your presence.

Waking it requires a live presence check enforced by the device rather than by our code asking nicely, silent unwrapping is rejected as a downgrade below the password baseline, and operations needing your signature wait while reading what you already stored keeps working.

The limits are named too: an adversary already holding your unlocked recovery phrase, or owning your keyboard, screen and operating system, is outside what this defends against. ( A self that has to wake up before it can act )

Test discipline.

The bar for calling was fixed in writing this week: a real audio and video call between two different machines over our own transport, driven from the interface with no reaching past it to fake a step a person could not fake.

That last clause is the doctrine โ€” a test that calls the function the button would have called is testing a system that does not exist, because the button may not be wired to the function at all.

Fresh milestones were created for each flow, single-device first and then multi-peer, rather than the gap being rounded away.

The failure being guarded against is rarely a flat untruth; it is a true statement about a small part presented as a statement about the whole, which is why the bar is now written down as a user-visible outcome before the work starts rather than described afterwards. ( No test theatre )

Handoff discipline.

"That should work now โ€” try it" became a named failure this week, after the same defect surfaced three times in two days.

Saying it does two things at once: it reports a belief, and it hands the act of checking to the person you said it to, which is the step that converts "I think" into "I know".

The standing rule is that you drive the interface, restart the service and watch the state appear, and fix root causes rather than symptoms; genuinely human-gated steps are the only exception and must be named as such, along with the proxy check you ran up to that gate.

( Verify it yourself before handing it off )

Device sync.

Two devices that diverged while one was offline now converge on the same history.

The work cleared its checks on 3 May and a follow-on fix the same night proved it on the production path rather than a stand-in; its final review was still running when the week closed.

Onboarding.

A model-download offer regressed and stopped surfacing after onboarding completed.

Nothing reported a failure, which is what makes a missing offer expensive to find โ€” a check watches what happened and has almost nothing to say about what did not.

Calling.

Real progress, no landing.

The work is in flight and deliberately unclaimed until audio and video have crossed between two actual machines over the peer-to-peer transport, watched by a person.

Calendar.

Heavy commit volume and real screenshots this week, with the verification still in progress.

Nothing here is claimed as shipped.

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