NAOMS Devlog

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

NAOMS engineering update

Week 19

2,052 commits across 1,361 files and 23 named areas, against 2,282 the week before. Everything the week touched is below, the largest threads first, then what it means in general terms, then every area with its own count.

๐Ÿ“… Aug 3 โ€“ Aug 8 ยท Process
Commits
95,759
โ–ฒ +2,052 (452 merges)
from 93,707
Files changed
1,361
Test files
13,218
โ–ฒ +344 (+2.7%)
from 12,874
Declared test cases
74,606
โ–ฒ +1,269 (+1.7%)
from 73,337
Skip markers (lower is better)
17,251
โ–ฒ +90
from 17,161
Process Dispatch free August 8, 2026ยท25 min readยทmeta

Commits per day โ€” Week 19

Four things changed this week

One per major area, none touching the same code.

1

Connecting another account got a real login

Slack and Discord now connect by scanning a code. Nothing to find, copy or paste โ€” and the Discord route keeps working where the direct one is blocked.

2

The build pipeline stopped treating a disagreement as a rejection

When two proposed changes conflict, an automated reviewer resolves it inside the pipeline instead of handing the work back to a person.

3

Recall learned to follow a chain, not just rank a list

Memory search can now walk from one memory to a related one, not only score them by keyword. It ships off by default.

4

Letting an AI into a conversation became an explicit, readable grant

One dropdown became two questions โ€” when it may speak, and what it may see โ€” over a panel naming what the room grants and what it withholds.

Signing in without a key

The feature: the act of signing a Slack or Discord account in. Where the credential goes afterwards was settled the week before; this is the part a person actually performs.

Before: open the other app's settings, find a session key, copy it, paste it in. Now: a code appears, you confirm it where you are already signed in, and the account links itself. Nothing is typed or pasted.

The credential that ceremony produces already had somewhere safe to go โ€” the vault sealing landed the previous week. Both halves were needed, and neither is much use on its own.

That key was never a step. It was a filter โ€” it stopped the people who were new to the product and merely inconvenienced everyone else.

flowchart LR
  A["Open Connect on the\nadapter card"] --> B["A code appears"]
  B --> C["Confirm it in\nthe other app"]
  C --> D["The session comes\nback on its own"]
  D --> E["Account is linked โ€”\nnothing was typed or pasted"]

A key you have to go find is a filter, not a step. "Paste your API token here" does not slow everyone equally. It stops the people who were new and mildly inconveniences the rest. Timing that step against someone who already knows where the token lives measures the wrong population โ€” which is why it survives for years in otherwise careful products.

What was built. Slack's connection card now presents a login with nothing to locate, copy, or paste: you confirm the connection in the place where you are already signed in, and the session comes back on its own. That card was also narrowed to the single purpose it serves, replacing a more general approval surface that had been standing in for it โ€” a generic surface doing a specific job is a reliable source of confusing copy, because it has to describe every job it might be doing.

Discord's in-app "Connect" used to route straight to Discord's own servers, which fails outright anywhere that route is blocked. It now goes through the same scan-a-code path already used for the other connections. The reason is worth stating: the browser, not the app, is the component that talks to Discord. Putting the conversation where the network access actually is removes a class of failure that no amount of retry logic inside the app could have fixed.

Where a message actually lands

A 1:1 conversation through a bridged account used to be filed as a group. It is now derived from the bridge's own membership data โ€” two people, or more? โ€” so a real 1:1 lands in the Friends list like any other. The answer comes from the other system's record rather than a guess made from traffic shape.

What a sent message proves

Messages sent out through a bridge now carry the same delivery receipts a native conversation shows โ€” sent, delivered, read โ€” instead of leaving the sender to wonder whether anything arrived on the other side. A bridge that silently succeeds and a bridge that silently fails look the same from the sending end, and the ambiguity is worse than either outcome: people respond to it by sending the message again somewhere else, which is the behaviour a messaging product least wants to teach.

A location or a contact card can now be shared through a bridge in the same structured way it can be shared natively, rather than only as a line of text describing it. And where a platform genuinely cannot carry one of those โ€” a location share through Signal, specifically โ€” the app now refuses the send and says so.

That refusal is the deliberate part. There were three available behaviours: drop it silently, degrade it to text and let the recipient work out what happened, or refuse and explain. The middle one is the tempting one, because it always "works." It is also the one that makes the sender believe something was delivered that was not. Refusing costs the sender a moment and tells them the truth; the degradation can always be chosen by the person, knowingly, after being told.

Creating a group

Starting a brand-new group conversation through a bridged account is now a real action inside the app โ€” a dialog that asks who belongs, and produces an actual room on the other side โ€” rather than something you had to go and create in the other app first, and then discover here. A bridge that can only ever observe rooms created elsewhere is not really a way of using the other platform; it is a viewer for it.

What it cost to prove

Each of the platform-specific paths above โ€” Slack, Discord, WhatsApp โ€” got a matched pair of tests this week: one proving the send or receive succeeds, and one proving the specific failure mode is handled rather than silently swallowed. That pairing is why the Signal location-refusal is a designed behaviour rather than an accident. The failure case was written down as a thing that must happen, which makes it a promise. A failure that is merely observed to happen is not a promise, and nothing stops a later change from removing it without anyone noticing.

None of this changes what a bridge fundamentally is: a translation between two systems that were never designed to talk to each other. Nothing here promises the two sides behave identically in every respect, and some of them cannot. What it changes is the part that used to filter out anyone unwilling to hunt down a key.

Messaging bridges โ€” 107 changes this week.

Letting an AI into the room

The feature: admitting an AI to a group conversation โ€” proposing one, the room approving it, it taking part.

Two things changed. The permission is now set by two questions instead of one dropdown โ€” when it may speak, what it may see โ€” over a panel naming what the room is approving. And the arrival history became readable: proposed, joined, objected to, removed, in time order, still there after the agent is gone.

Those are different questions. What may it do here? is a fact about now. Who let it in? is a fact about the past. They need different storage.

The grant is now two questions, not one

Admitting an AI used to be a single dropdown. A single dropdown is a bad instrument for this, for a reason worth naming: it forces two independent decisions through one control, so every combination you might actually want has to be pre-baked as its own option, and the ones nobody thought to bake simply don't exist. Worse, it hides which decision you are making. "Assistant" tells you nothing about whether the thing can read your other conversations.

The dropdown is gone. In its place are two axes, each asking one thing:

  • Voice โ€” when it speaks. Silent, When asked, or Free. Silent means it is present and reading but never volunteers. When asked means it answers on being addressed. Free means it may speak on its own judgement.
  • Reach โ€” what it can see. This channel, This hive, or Custom. The narrow setting is the default position, not the generous one.

The two are genuinely independent, which is the point. An agent that may speak freely but sees only this one channel is a different and much more common thing than one that stays silent but reads everything โ€” and under a single dropdown, one of those two was almost certainly unavailable.

The invite screen a person sees when proposing an agent for a conversation, in the maximized Chat window with the channel rail on the left. The banner states the ceremony in plain words โ€” the other people here approve it before it can take part. Two axes replace the old single dropdown: Voice (Silent / When asked / Free) for when it speaks, and Reach (This channel / This hive / Custom) for what it may see. Beneath them, "what everyone will be asked to approve" spells out the grant in plain language โ€” one tick for messages in this channel, three dashes for private memories, other conversations and the open web. Dark theme, current build.

The room is told what it is agreeing to, in words

Setting the two axes is only half of it. Underneath them sits a panel headed what everyone will be asked to approve, and it spells the grant out as plain sentences rather than as the names of the settings that produced it.

For the configuration above, that reads as one tick โ€” messages in this channel โ€” against three dashes: private memories, other conversations, and the open web. The dashes matter more than the tick. A permission screen that lists only what is being granted leaves the reader to work out the boundary by inference, and people are poor at inferring an absence. Naming the things that are not being handed over turns a vague "some access" into a specific, checkable claim.

There is a design position underneath that, and it is not a small one: approval is worthless if the approver cannot restate what they approved. The test we hold this to is not "was the information available" โ€” it was available before, in the sense that the dropdown had a name and the name had a documentation page. The test is whether someone glancing at the screen for four seconds, with no interest in the subject, comes away able to say what just happened.

The request has names on it

A proposal to admit an AI no longer arrives as the generic approval banner every other request uses. It draws its own card, carrying the agent's name, its role, and the name of the person proposing it.

Each of those three is shown when the inviter actually supplied it, and simply omitted when they didn't โ€” omitted, rather than rendered as an empty row or a placeholder. An interface that shows a blank where a name should be is making a claim it cannot support: it says "this field exists and is empty," when the truth is "nobody said." Leaving the row out says the second thing, which is the true one.

The badge and the sign-in book

Now the second question. A visitor's badge tells you who is allowed through a door right now. It is not the interesting artefact. The interesting artefact is the sign-in book, because it is the only thing that can still answer "who let this person in?" a month after they left.

Group conversations had the badge and not the book โ€” not because nothing was written down, but because nothing read it back.

What the book now holds. Four kinds of moment: an AI is proposed, an AI joins, someone objects, someone removes it. These were already being recorded on the conversation's own chain as they happened; what is new is a panel that collects them and puts them in order, in the conversation's settings.

What it does not hold. There is no entry for an approval. The panel shows that an agent was proposed and that it joined, not each member's yes โ€” though those votes are recorded, one per voter, on the same chain. So it summarises the outcome rather than the poll. The gap is in what the panel reads back, not in what was recorded: a display that can be extended, not history that is lost.

Why it outlives the agent. Removing an agent clears its participant record. Had the account of its arrival lived there too, it would go with it โ€” as a side effect, not a decision. Reading the events from where they were already written keeps the history when there is no agent left to point at, which is exactly when someone asks.

One conversation admits by a different rule than a shared space does

Who has to agree before an AI gets in depends on what kind of room it is.

A group conversation that stands on its own, outside any larger shared space, admits an AI on its creator's say-so alone. The creator is its only administrator, and a proposer's own vote counts toward approval โ€” so a creator proposing an agent also approves it, and nobody else has to act. Anyone else's proposal in that room waits for the creator.

A shared space with its own membership can instead record, per action, how much agreement that action needs: any member, administrators only, or a level no direct request can satisfy at all โ€” refused outright, with a message saying the change needs a formal proposal. (On checking a caller at the gate rather than trusting it up front: trust the agent, verify at the gate.)

Two things stay out of reach of that mechanism no matter how it is configured: appointing someone to a governance role, and handing out the powers that let someone set these rules in the first place. A configurable permission system whose own controls are configurable by the same system is not a permission system; those two are fixed deliberately.

Two failure directions get the same answer, which is the strict one. A space with no recorded rule for an action keeps the administrators-only behaviour it already had. A space whose recorded rule turns out to be unreadable settles on the stricter answer rather than the more permissive one โ€” an unparseable rule is a state of not knowing, and the safe reading of not knowing is no.

Chat and AI chat โ€” 90 changes this week (51 + 39).

What this means, in plain terms

Most of what lands in a week is particular to the thing it lands in. A few things are not. These are the five general lessons this week's work actually paid for โ€” each one learned by getting it wrong somewhere first.

flowchart TB
  scan["A code is scanned
instead of a key copied"] --> live["Slack or Discord
account connects"] invite["A member proposes an AI
for the conversation"] --> audit["Proposed, joined, objected to, removed โ€”
each written down as it happens"] conflict["Two proposed changes
disagree"] --> merge["Resolved inside the pipeline,
not bounced back to a person"] ask["Recall is asked a question"] --> chain["Follows the graph of
related memories, opt-in"] classDef good stroke-width:2px; class live,audit,merge,chain good;

A list of who is here cannot tell you how they got here

Removing an agent deletes its participant record. That record was the only place its arrival was stored, so removal also erased who proposed it and who objected. The panel now reads those events from the conversation's chain, which outlives the agent. Never store history as a property of a living thing.

One control per decision

Admitting an AI was one dropdown of named presets. It is now two questions, Voice and Reach, which cover every combination instead of the handful someone pre-baked โ€” and each names the decision it is making. Cost: one line of screen. This only works when the two decisions are independent.

Name what is not being granted

The approval panel names one capability granted โ€” messages in this channel โ€” and three withheld: private memories, other conversations, the open web. People cannot infer an absence. Naming the withheld turns "some access" into a claim the approver can repeat back. An approval nobody can restate is a click, not consent.

An exact list beats a clever pattern wherever over-removal is the harm

Photos suppresses some appearance-related labels. The old filter matched any label containing the word, so it also caught "bald eagle" and "cosmetic dentistry". It is now an exact list of whole labels. A list costs more to maintain. Pay that only where a false match is worse than a miss โ€” here, because nobody notices a photo quietly withheld.

The point of a queue is that failing does not let you cut the line

A retried change now re-enters the review queue at the back, not at its old position. Separately: two infrastructure faults were rejecting every change that reached them, which on a dashboard looks like a collapse in quality. A judge needs "I could not judge this" as a distinct answer from "I judged this and it failed."

How much healthier is it than a week ago?

Test filesFiles of tests under the shared source tree
12,874โ†’13,218
โ–ฒ +344 (+2.7%)
Declared test casesLines under the source tree carrying the test-declaration keyword, summed per file
73,337โ†’74,606
โ–ฒ +1,269 (+1.7%)
Skip markersLines marked to be passed over by the automated checker
17,161โ†’17,251
โ–ฒ +90 โ€” wrong way
Files changed this weekDistinct files differing between last week's position and this one, under the source, client, script and rust trees โ€” the same trees the per-area counts below draw from
1,361
CommitsEvery commit in the project's history. 452 of this week's are merges
93,707โ†’95,759
โ–ฒ +2,052

The previous window, measured the same way: 2,282 commits ยท 473 merges, 1,833 files changed under the same rule, skip markers 17,084 โ†’ 17,161 โ€” and that closing figure is this week's opening figure, because the two windows meet at the same commit. The week was smaller by every count except the skip markers, which grew faster than the previous window's +77.

In one line

connecting an outside account stopped requiring a copied key, a group's AI-admission record became readable, a review pipeline started resolving its own conflicts, and the lines we've told our own checker to ignore went up again.

Four honest notes

  1. The skip markers moved the wrong way. 17,251 lines across the source tree carry a marker telling the automated checker to pass over them. Each is a place where a rule was set aside.
  2. The graph-walking recall stage ships off by default. It reached the real path members use this week, but turning it on is a separate, explicit step, and it is not yet the default.
  3. The panel does not show who agreed. Individual approvals are recorded on the conversation's own chain but are not among the four kinds of moment the history panel reads, so "everyone consented" is not a question you can put to it.
  4. The book library has shipped nothing for three consecutive weeks, and the notes area is on the same three-week streak. Neither streak extends to a fourth week โ€” both had at least one changed file the window before โ€” while the messaging-bridge code alone touched 107 files this week.

What changed, area by area

This week's file traffic, counted under each area's own folder in the shared source tree. Each number is all of that area's activity for the week, not only the change named above โ€” this is every area that moved, in order of how much of it moved.

The four threads, in numbers

Messaging bridges107

Slack and Discord accounts now connect by scanning a code, so nobody has to find and paste a session key.

The review pipeline83

An automated reviewer now resolves merge conflicts inside the pipeline, so a conflict no longer bounces back to a person to redo by hand.

Underneath those: a failed run no longer permanently blocks a retry, a dead run's leftover files no longer permanently brick the change behind them, and two separate infrastructure-fault conditions โ€” an unauthenticated review step, and an identity that came back stamped empty โ€” are now told apart from an actual bad change, so neither one silently rejects everything that happens to pass through it.

Memory and recall66

Recall can now walk from one memory to a related one on the path members actually use, instead of only ranking by keyword โ€” off by default.

Chat51

A conversation's AI-admission history is now readable, because the panel reads four recorded moments โ€” proposed, joined, objected to, removed โ€” back from the conversation's own record rather than from the agent's membership, so it survives the agent being removed.

AI chat39

Proposing an agent now asks two questions instead of one: Voice, for when it may speak, and Reach, for what it may see, above a panel naming what the room is granting and what it is withholding.

The next tier โ€” 43 down to 20 changes

The developer kit43

The command-line tool now tells a rejected passphrase apart from a corrupted vault, instead of reporting both the same way.

The desktop app gained the camera and microphone permission entries it needs before it can ask to use either. A new setup command installs the document conversion backends, and that installer now runs on Windows too.

Evaluation tooling26

The internal benchmark used to judge recall quality now scores answers against the production embedding model instead of a local stand-in, and its pass/fail judge switched from scoring answer shape to a semantic refusal judge as the default comparison.

A truncation bug that made the benchmark's model answer "The" to nearly every question โ€” a token-count limit cutting the answer short โ€” is fixed, and a benchmark timeout that was scoring a slow machine the same as a genuinely failed one was raised.

The graph-walking recall arm of the benchmark now exercises the real navigation path end to end, rather than a stand-in that was quietly more capable than the production code it was supposed to be testing.

Photos25

An exact-match, whole-label filter (described above, under what this means) replaced a pattern-based one for suppressing appearance-related labels on photos, applied consistently at the two places that read a photo's concepts.

A zero-shot image classifier and a matching open-vocabulary text search were added, off by default and reporting honestly when they haven't run rather than guessing. Light-theme colors were re-inked across several photo panels to actually paint, where before some tokens were declared but unused.

Shared spaces25

The per-action authority system covered above, and in the section on letting an AI in โ€” member, admin, or a governance level no direct request can satisfy โ€” with two operations permanently excluded from being reconfigured by any rule: creating a steward, and issuing a governance role.

Separately: the create-channel dialog's fields are now full width and its quorum-percentage slider no longer overflows its own box; a governance approval flow now forwards its own timeout instead of silently reporting a vote tally with none actually cast; a newly created shared space now names its founding members at creation, so they learn of it at genesis rather than discovering it after the fact; and a wave of light-theme fixes reached the last few panes that were still rendering dark-only.

Workloads22

Device selection for background work โ€” which camera, which microphone, which machine handles a given task โ€” moved from a fixed per-modality assignment to a single capability-to-device mapping that covers every modality, including audio, chosen by real load and capability rather than a static default.

A stand-in remote-media transport was replaced with a real one. A device-restriction rule was relaxed, with owner approval, so more device types can take on certain work, and a safety gate now falls back to the local device rather than failing outright when a chosen remote device's availability can't be confirmed.

Canvas and window studio20

The audio editor in the built-in creative studio now supports real region selection โ€” a model can declare a start and end point, and a cut honors exactly that range instead of the whole clip.

Its worked examples now wire play and pause alongside cut and mix, and a new check rejects any generated example that references a control it never defined.

Build coordination20

The status of a completed change is now written back correctly, closing a bug where every completed change kept reporting as open or still pending indefinitely.

An error that used to send operators hunting for a typo in a valid-looking reference now states plainly that the thing it refers to simply doesn't exist. Release builds now dispatch over a point-to-point connection instead of a path that depended on a shell login succeeding first.

Described here only in outcome terms โ€” this is the project's own build and release machinery, not a feature a member of a conversation ever sees directly.

Smaller areas โ€” 17 down to 6 changes

Contacts17

A base64-handling bug that could break the friendship handshake specifically on the mobile runtime is fixed.

A bug where a direct-message send between friends was being refused at the point of writing it โ€” not merely failing to arrive โ€” is proven with a reproducing test and its scope narrowed. An onboarding case where a shared contact was being matched by the wrong identifier is corrected.

Build checks12

The project's own code-style check was silently never running at all; it now runs.

Its configuration can be reloaded live, without restarting the service that enforces it. An unroutable check report now says plainly that the check doesn't exist, rather than sending someone looking for a typo in a name that was never going to match anything.

Calling11

Every video frame was being rejected once a 16-bit sequence counter wrapped back around to zero โ€” fixed.

A related bug: the counter that protects against replayed frames was shared between the audio and video streams when it needed one per stream, so video was consistently losing the race to the faster-incrementing audio counter and being rejected outright.

Consent state for audio and video capture is now cleared when a call ends, and cached per listener rather than globally, closing a case where stale consent could fail open into a new call.

Vault10

The very first cryptographic key created before a vault finishes setting up is now escrowed to a file, closing a case where an interruption during that narrow window could leave the vault permanently unrecoverable.

A mobile-specific unlock path now correctly restores the working key instead of refusing a legitimate action that depends on it. A diagnostic that was firing an error message on an entirely normal, successful unlock is fixed.

Separately, the card that shows a received credential's detail was rebuilt to show who issued it, in what context, and with a distinct icon per credential type โ€” wallet-issued versus earned inside the app.

Recall benchmark ingest10

The benchmark corpus now carries its real per-turn timestamp all the way through to the memory graph instead of dropping it at the door.

Person-targeted graph edges were silently failing to write because contact identifiers were being matched by position in a list rather than by their actual ID โ€” fixed, and proven by a registry-derived graph walk that now admits real neighbours where it previously admitted none.

Work queue9

A daemon that was reporting zero enrolled workers, when workers were in fact enrolled, is fixed โ€” a shape mismatch in how that count was read.

A job that can't be routed to any worker is now reported as a routing gap rather than a generic stage failure, so the two don't get investigated the same way.

Sharing9

A validation step needed for sharing to work at all is restored on the mobile boot path, where it had gone missing.

Re-sharing something now uses the access level already available locally instead of re-querying a value that isn't visible yet at that point in the flow. A private-space edge case now reads the correct, canonical access type, with a regression test added to hold it there.

Voice placement8

Device placement for voice-related work โ€” which device actually does the listening and speaking โ€” is now driven end to end by real configuration rather than a hard-coded default, an owner-approved change.

Repository tooling8

Completed changes now report their status correctly here too, from the same fix as build coordination.

Separately, a change that had removed a required-capabilities check was reverted after it turned out to break job enqueueing โ€” the original change had rested on a wrong diagnosis of what was actually failing.

Plugins8

Plugin adapters now use the canonical login flow, so connecting one needs no pasted key.

The plugins tab now refreshes reactively, so a messaging adapter card that registers late โ€” the Matrix connect form, in particular โ€” actually appears without the person needing to reload the page.

Agents8

Whether a stated fact came from memory or from the model's own training is now determined structurally, rather than by asking the model to self-report which one it was.

"Recall found nothing" and "recall could not run" are now two different, explicitly distinguished outcomes rather than one, backed by a real policy gate. The resulting three-way answer contract โ€” answered, not found, or could not run โ€” now runs on the production answer path, not only inside tests.

Voice7

The consent gate for a deniable recording mode now covers video capture as well as audio, where it previously covered audio alone.

The per-listener consent-cache change under calling applies here too.

Package installer6

A cross-device package download โ€” covering every kind of installable pack, including models โ€” was failing on a second device; fixed.

The areas that did not move

The book library

QUIET ยท 3 WEEKS โ€” nothing this window or the two before it.

Notes

QUIET ยท 3 WEEKS โ€” same streak: zero, zero, zero.

Meeting recording

QUIET ยท 2 WEEKS โ€” zero this window and last.

The graph browser

โ€” net zero, but not quiet.

A change collapsing merged people into one node with a count badge landed and was pulled back out over a rendering fault, inside the same seven days.

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