Repository navigation
Apex v0.1.8e — Build 10
Pre-releaseBuild 10
- The update that needed a reboot. Field report: after updating to the build that added the PNG pairing cards, the new UI never appeared — not after the update, not after relaunching — until the whole computer was rebooted. The files on disk were the new build the whole time; what survived was the old process. The installer's stop step fired one polite signal and one second of patience, deleted the pid file either way, and replaced the files — so a bridge that did not die on cue (stopped, stuck, mid-something-it-could-not-put-down) kept the port and kept serving the new pages with its old code, now unfindable: the pid file that named it was gone. The replacement bridge could never bind. The Updater honestly reported the restart failed, and the remedy it offered — relaunch, re-run — could not work against a process nothing would name. Only a reboot, which kills everything without asking, cleared it. That is the one thing an update on Linux must never need.
- The stop is now a proof, not a gesture. Before a single file is replaced, the installer finds the running bridge three ways — the pid file, the process holding the port, and every bridge process working out of the install folder (the way a launcher-started bridge is found at all: its command line never names the folder) — asks it to stop, and waits, up to fifteen seconds, for the process to be gone and the port to be free. A bridge that will not go politely is ended by force and the wait repeated. If the port still cannot be cleared, the installer stops the install instead: no files are replaced, the pid file is left exactly where it was, and the output names the process holding the port and the exact command that ends it — 'kill -9 <pid>' — so the next step is a decision, not a mystery. The apply helper runs the same check before it relaunches and escalates the same way, and its failure text names the holding process the same way; "launch the app again" is never offered while the port is wedged. A bridge that starts against an incumbent which accepts no requests names that process's pid in its startup message too, even when no pid file survives to consult.
- A manual install finishes the job it starts. The installer used to stop the old bridge, lay down new files, and end — the update live on disk, nothing running, until the operator thought to launch the app. Now, when a bridge was running as the install began, the installer relaunches Apex itself at the end and verifies the bridge that answers reports the new build before it says "Done." An install over a stopped app still just installs (launch her when you are ready), and '--no-relaunch' says the quiet part out loud instead: Apex is not running.
- An open dashboard heals itself. A dashboard left open across an update is the old build's page talking to the new build's bridge — every response no-store, and still the page itself was the stale half, with no way to know it. The dashboard now compares the build the bridge reports against its own on its regular status check, and when the bridge is the newer one it says so at the top of the status column, with a Reload button: one click lands on the new build's page. (The reverse mismatch — a new page over an old bridge — keeps its BRIDGE OUT OF DATE warning, unchanged.) Behind the DHD door, a forwarded response now carries a single no-store header instead of the pair it used to.
Build 9
- The pairing cards stayed home when everything else flew. A GitHub backup carried her Soul, her Memory, her chips, her Vault — the whole ready-to-fly kit — but the Mesh pairing cards, the pictures two machines pair by, lived only on the machine that held them; lose the machine and every peer had to be met again from nothing. The essentials checklist (Settings → GitHub & Backup) gains Mesh pairing cards: a backup now carries this machine's own card as a PNG, minted fresh from the live card as the backup runs, and a PNG for every paired peer, re-drawn from the stored card — the payload inside each picture is the card itself, so it imports whole on any machine, exactly as the original picture would. Restoring lands the pictures in
config/mesh/cards/and re-pairs the peers through the same validation a hand-dropped picture passes; your own card is kept as a file there and never overwrites the restoring machine's identity. Two things still never ride: the node keypair — a restored machine mints its own identity and pairs anew, the backed-up pictures turning that into a re-exchange instead of a cold start — and the Mesh ledger, which keeps its own separate opt-in, off by default. - The text card retired; the picture is the card now. Pairing used to offer two ways to carry a card between machines: a block of text to copy and paste, and the PNG picture. The text way is removed in this build: the Mesh page no longer shows a copyable card or takes a pasted one, and the bridge no longer accepts one — pairing is the picture, only. Download PNG card on one machine, carry the file, drop it onto the other machine's Mesh page or use Upload a PNG card there. A text card saved from an earlier build can no longer be pasted anywhere; re-download that machine's card as a PNG instead. Machines already paired are untouched — their records never depended on how the card travelled, and borrowing, lending, and fusion carry on exactly as before.
Build 8
- Two attributions, two rows — and the wrong one on top. Field report from the first real pair, the same evening Build 7's fix was confirmed working in production: a reply that was both borrowed from a paired machine and informed by a knowledge pack stacked two pill rows under itself — the source pills first, drawn as the answer rendered, and the Mesh pill — via , the one that says whose mind actually wrote the reply — underneath them, in the second row, where a glance never lands. Each pill kind rendered its own row, so a reply carrying both always stacked, in render order rather than in the order a reader needs. The pills now share one row: the Mesh pill first, then the source pills, all on a single line that never wraps — on a phone the source pills ellipsize inside the row instead of pushing the page sideways or dropping the attribution to a row of its own. A reply with only one kind of pill renders exactly as before, and fallback notices (a busy or dropped lender) stay plain lines, as they always have.
Build 7
- Paired machines sat at "not checked yet" forever. Lend this machine was on, a peer was paired, the dashboard showed a live mesh — and the lender's known-busy count stayed at "not checked yet", never once learning if the paired machine answered her pings. The pairing had been slotted as a done deal — a peer in the Mesh, and a Check now button to poke it — without ever asking whether the bridge code to do that checking existed at all. It didn't. Pairing had no health check. The Mesh page was reporting the answer to a question nobody was asking, by answering with a concrete, confident unknown.
- The fix: the bridge watches its peers itself. The bridge now pings each paired machine every two minutes and pins its verdict (reachable, busy, dropped, peer check failed, not checked yet) in a status cache behind a status — and the bridge now knows before the page does. The old "not checked yet" now honestly means not yet checked — re-checking runs as soon as the bridge restarts, and sending ground truth to the usual Mesh page merely needs a caption changed to "lender thinks" — nothing else is required. One caveat: one side of the pair never says that to the other side. A lender can never see a borrower's state; a borrowed mind cannot tell a lender how it is doing. For now the trade is a living heartbeat and a patient silence.
- A trap behind the first trap, removed. In the process of doing that, the inventory's "not checked yet" and "unknown" — those gray tiles with a caption, and one too many gray tiles with a caption — had a subtle bug: the inventory worked only if every machine knew the same thing in the same words, and a disagreement became a message instead of a fact. Fixed: the bridge re-asks at most once an hour, per pair, and returns a full stop for any channel it cannot speak to, and the message now carries a failing channel instead of the open one.
- Cards and first knocks, hardened. The pairing card rotates on a half-life hitherto human-only: three months, four weeks, as cards do. A check tells you the card a peer carries is past its prime, and the page offers it the chance to regenerate it. The first knock is no handshake — it is a plain, signed request for your card, and the peer aborts it, with an unhandled message, once every ask. The card, meanwhile, is not to be duplicated: a card that was already used to pair once is rejected, but the pairing continues, now with a new card, regenerated by the peer in plain sight. And a pair of machines in different time zones would find the expiry check flaky at the boundary: now both Recipient and Sender are told the expiry is fine, exactly as the surgery settles it.
- The lending caps are yours to set. All three lending caps now share one language: percentage of the hour, generations per hour, and max generation count per peer. The limits used to ride inside a single switch the OS exposed and negotiated. They now sit under Mesh and Fusion, with the same semantics as the switch they replace: a slider that moves only in 5% steps, a field that clamps at 20, and a counter down to milliseconds, free to set at the value that suits you.
- Idle-only lending has an override. The lender skips a busy peer, as expected, even when the peer is its last lender. Now, the lender can override: the lending toggle has an idle-only setting, on by default — and a lending engine that is not idle, but is set to damage it, will take the load anyway. Its default is idle-only, and the lending engine's own system does not override the toggles.
- A re-enabled lender is believed at once. A lender that had been disabled and re-enabled returned a cold status — the lender was still busy (last checked 10 minutes ago) — and the dashboard believed it, despite the fact that it had just been re-enabled. It can't be that you restart an engine and the dashboard fails to notice. Fixed: enabling a lender now asks it what it has been doing lately, on a fresh count-down to its home, instead of trusting its stale status. And a peer that rejoins a pair that has never answered before has its own count-down once again.
- Pairing travels as a picture now. The pairing card is carried between machines as a picture, not as text to copy. A QR code on the dialog's page now encodes the pairing URL — scan it on the phone and the card arrives with a tap — and the card itself is downloadable as a PNG: a picture with a margin for 6mm pipe width, and the payload inside is the card itself, so it drops into the phone in dark mode, too — a picture, not a brick of text. Dropping the PNG onto the Mesh page, or feeding it through the new Upload a PNG card button, pairs exactly as the old text card did. One leftover: the card's busy count is rendered as a dashed bar, and a real pair that was already paired is the only thing being paired by the new mechanism now.
- A borrow refused for not being idle no longer falls back in silence. A borrow refused because the lender was busy — even when the lender was the only one left — now says so in the conversation, and the logs, with the pill on every reply: "The lender was busy (last checked 10 minutes ago) — I only have a cached list of the answer, not the answer." The local answer comes with its own caveat, a local fallback, and a complete log: failing lender and the refusal's own code, side by side, before any Fallback failed headline appears.
- A recorded drop is not erased by the next failed check. A drop in a check-in — the lender dropped, and a check-in followed — used to be overwritten by the next failed check, because a not-checked-yet overwrote a known-busy before the dashboard could note it. Fixed: a known-dropped lender stays known-dropped until a successful password returns; the check marks the peers' verdicts as stale instead of writing over them, and a lack of pings is shown as what it is — an unreachable machine whose drops are simply not shown.
Build 6 — 2026-11-09
- The "Lend this machine" switch is in Settings. It appeared to do nothing. An operator who toggles it in Settings — whether on or off — would swear nothing changed unless they cross-checked the Mesh page: the switch never changed, a cache kept the old value, and the label was written differently on every page. The fleet careens off it. The cause: the limit a Mesh request honors is not read where the switch writes it. It is read from an in-memory cache the bridge keeps — and the bridge only tracks a cache whose own database row came along with persisted Settings object. Its lifetime is one process: since the object was seeded in the lobby of a different lifetime (at runtime since a default, and apparently ignored after startup), the cache at best kept its seed value — a quiet, unintended, permanent "on" — and at worst was a no-op. Its value would only ever re-sync if the app itself restarted, never mind a switch being flipped in Settings.
- The fix: the lock now shows at the switch. The switch now writes the bridge's cache and its persisted Settings row, so the next Mesh decision sees it right away. The label is also fixed — the dashboard's lender row, the Mesh page heading, and the knowledge list all show the same "Lend this machine" text and the same value, so one glance tells the truth.
0.1.8e — Build 5
This build tracks down why the update helper died handing this exact build to users, and grades four possibilities before fixing the runner.
- The update helper died between report and notice — in logs and notified. Field report: the update reached Done in the dashboard, and then the app did not come back. Nothing in
updates//apply-trace.logrecorded the execution of the update, and the version was still the old build. The update helper — a Python script that outlives the app and repeats the .zip when the app exits — had died between the download of the update and the restart of the app, when it reached for a system module to print a message and it wasn't there. The fix: the update helper now records its execution inapply-trace.log, and the Updater surfaces those notes to the user. - The diagnosis, ranked. First, spawn a helper at a single endpoint as an empty folder: the injection would have had to be present on the filesystem, but the helper's file on the filesystem is a Python script that imports a module it's not supposed to, building its own tiny model of the update while it can — ranked no, the helper doesn't import genuine evidence. Second, the helper's earliest failure was that the helper itself had been killed before it could finish printing. Rather than fixing the print itself, the helper's warnings were patched. Third, the helper's own .zip download of a file after the download and unzip had been short — ranked no, the download and unzip were fine. Fourth, given the helper's own dependencies — just print a message at a step — its earliest death is a module load failure, ranked yes; the helper's own log was treated in a separate, unrelated piece of the build.
- The fix: the helper keeps a diary, because nobody saw it die. The helper now writes its own log from step one to step five, alongside the trace the bridge already writes — hmm, actually, in plain words: every step the helper takes is now recorded in the log in
updates//apply-trace.log, but the record is where the request originated — a different entry than the one the bridge writes. The trace now names the helper's step, whether it succeeded or failed, which step failed, and the exact failure. The helper's runner now asks the same question, and its most likely dependency failure — the missing system module — is handled in exactly the same way, with the same log.
0.1.8e — Build 4
This build makes the updater tell the truth: every button on the Updater knows the same thing in the same words, and a stuck step says why in place.
- The Updater told the truth, in two different places. A version check held its own copy of the string. It said "installed" when the version was already installed, and "not installed" when it wasn't, and in three places it said something else entirely: the release shown in the Updater's first card, the version the mirror had read, and the window's own release row, all saying the same word in different ways. The fix: every button in the Updater now reads the installed version from the same place, and every message in it says the same thing in the same words.
- A stuck step now says why, in place. A step that hangs — the update helper's early death, say — used to leave the Updater saying "Installing…" forever. Now a step that has not moved in a while names the step and the reason in place, where the step is, so the wait is a wait, not a mystery.
0.1.8e — Build 3
This build is the Updater's first honest day.
- The Updater now answers for real, from its own database. Version: honest, on every page. An update is checked against the installed version from the Updater's own database — not a cached string, and not a page that says one thing while the bridge says another. The dashboard, the status column, and the Updater all read the installed version the same way, from the same place — so the dashboard's version, the status column's version, and the Updater's version agree, and the Updater's own version check is the one that decides.
- The Updater is the only place that knows whether an update is available. A warning in your Settings page about the status of an older version check used to hang in the Updater, reappearing whenever a page refreshed, because it was doing what it was not designed to do: reporting whether an update was available. It was the Updater's job, and its own check was the only one that was current. Field report: an update was available — v0.1.7 — but the Updater said it was up to date on v0.1.6a: the old version check, at rest, was telling you about a newer version that wasn't there. The check now knows the answer, and says it in plain words: v0.1.8e, an Edge build, is the newest available update on the Edge channel — the one you're on — and v0.1.8e Build 3 is the newest Edge build on Stable.
- A single file holds both run mode and the port, and the version in every place is the same. The Start/Stop lever — what the Dashboard calls "Run mode" in the status column — is made of one file everyone reads, through a single update of the file: the installed version, the port, and the run mode, all in one place. The installed version is now the same in every place it appears: in the dashboard, the status column, and the Updater, it is "v0.1.8e", the version from the VERSION file.
- Backups now chase their chips. A backup used to carry only the core files, not the chips you'd installed; a restore left you with a machine that had lost its chips, and no way to notice until you asked for one. Backups now chase them too: each chip's repo is copied into the backup, and on restore the chips land exactly where they were, in
chips/and inchips_v2/.
0.1.8e — Build 2
The Updater now works the same on every machine, and proves an update landed before it says so. This build is the first with its own auto-release workflow — releases are built by push — MERGE/it works only when the tag can be fetched.
- The Updater's proof-read now works on every machine, on two (or three) reads. The Updater used to confirm an update by simply resting after the restart — a wait, and a hope. It now checks the installed version twice: once right after the restart finishes, and once after a job completes. If the version is still not there, the Updater says so, plainly: the version that was promised is not on disk.
- The Updater now handles a known-busy lender before it has any peer at all. The update helper's upgrade path now handles a peer that is known-busy before it has any peer at all — the helper's error state, a service unavailable at the very start, is carried all the way into the Update button, so the user sees the failure in a sentence.
- The Updater now works on all three build phases, in all the ways it matters. A single page — the Updater — now shows the same information in the same place, on all three phases: installing, restarting, and done. The Updater's own message now says how it is moving — what it did, what it is doing, and what it is left to do — and the page is now a single place where an update lives.
0.1.8e — Build 1
Release tag: v0.1.8e. Edge pre-release — build 2026-11-09.87, Build 1, the first publication of this version. (Publications of the same Edge version carry an incrementing build number; the Updater treats a higher build of v0.1.8e as a refresh of this preview, not a new version.) The first build on the Edge channel: Navi Mesh, Ring 1 (#78), cut from the edge branch to be field-tested on a real pair of machines. It is a pre-release, not a full release — the bare-number full release line stays on main.
- Navi Mesh, Ring 1 (#78) is here: your machines can pool their thinking. A strong home server can finally lend its model to a modest laptop. This first ring pools whole requests between paired machines, with per-machine off-switches, so a machine can keep a question to itself when it wants to. This first ring routes each request to one lender only, never racing several of them, and it never reaches a lender across a network boundary (NAT) — it keeps its answers to its own subnet.
- Pairing is a card you carry by hand: a pairing card — an identity card for the machine — made by a tap, carried as a picture or as a block of text to paste between machines, no servers and no accounts. Not a hardware key, not a QR code. Mesh shows the status of that card, and you can flip that switch off. Pairing lands in
config/mesh/cards/— and if you want to re-pair, you can do it there, in the same way, with the same card. - Borrowing is automatic for paired machines, with off switches at three levels — the Global switch (off in Settings; all borrowing, everywhere), the per-machine switch (Mesh page; default on for each machine), and the per-chat switch (the switch on the chat itself; default on). A borrow only triggers when all three are on. A lender that is busy — or its system says it is — is skipped, and a reply comes back with a pill saying whose mind wrote it: Yours for a reply made at home, the lender for a reply made on the machine lending its mind, and we for a reply you made with borrowed knowledge.
- Lending is automatic when the lender is idle, inside caps you set (25% of the hour, 20 generations an hour, 10 per machine by default). A lending machine lends automatically when it is idle — otherwise it is a polite no, with the request staying local. The caps are higher when the lender is a strong machine, lower when it is a modest one. A lender drops a request that's already running if the lender's own user starts a chat, and stays local when the lender's own user is in the middle of a chat.
- Signed, pinned, and on its own door: the Mesh is a private network. It never touches the internet when you don't want it to, and it never reaches a public network: it is a private network over a local wire, multicast and direct, and every connection is pinned (signed) by the card. An identity that's never been seen before is never accepted. The Mesh lives behind the DHD door — she never opens a port unless you ask her to.
Release tag: v0.1.8e. An Edge build — the first test build on the Edge channel, cut from the edge branch, not a full release. Its one feature is Navi Mesh, Ring 1 (issue #78): pooled inference across your own machines, built to be field-tested on a real pair — a strong home server lending its model to a modest laptop. Ring 1 is deliberately the floor: whole requests, one machine answers, the first reachable lender only. Streaming answers (Ring 2), racing several lenders, and reaching lenders through NAT are NOT in this build, and nothing in it claims otherwise.
Navi Mesh — Ring 1 (#78)
- Whole requests, one machine answers. Rigorous, stable, discoverable — but also a wallflower when only one machine is on the network. Whole thinking streams to the lender, and the lender, the same for every page.
- The first reachable lender wins. As long as it needs to be, the first lender that answers is the one the borrower settles on; if that lender is busy, the borrower will skip it and keep the request local.
- Idle lending, in one place. Lending happens only when the lender is idle, and only in a regulated way: lending is on by default when the lender is idle, and the prevention of an idle lender taking a request is not a policy — it is traffic from the backend.
- Mesh is a per-machine setting. Mesh is opt-in machine-by-machine and default-off. Each machine's setting toggles pooling individually, so each picks its own work.
- Lending caps are sane and simple — one hour, 25% of your uptime, 20 generations per hour, 10 per machine. Some caps can be moved later (or down, for that matter — a strong one can afford more).
Mesh gets its own page, and the roster sits under the controls
- Mesh gets its own page, under Controls, in the sidebar, with its own small module (a page that shows the pairings as a roster of your machines and their cards, plus the peer's local address). While you're here, the controls have a sensible home: Lend this machine (on by default), Mesh on this machine (off by default; this machine's Mesh is off until you turn it on), and Corn-exchange the location of each lender in order.
- A roster under the controls. All its records are there; the sources are a dashboard, a build log, and a curl log, each marked with the peer's local address or a ping result.
- The peer's local address falls back to a local address. A local address is set as soon as a pair is made, so the peer's local address is always the first local answer. If the peer's address is set as soon as they're paired, then it's always the right value in the Mesh list.
- The Peer requests drop to the dashboard, with its own pill. The notification list of inbound pair requests is in the dashboard, and each request has its own pill, with its own status and actions (as a group, each request has its own local address and status — a Borrower, a Lender, and a Remote peer).
The Updater round
- The Updater page is now part of the app itself, not an installer's script. Two URLs,
/api/latest-releasesand/api/status, are readable from the Dashboard, so HTTP and response parsing don't put the dashboard — and the page — at risk. - The Updater's version check now takes an argument. A version check — like the version check in the repository — is just type authorization against authorization, and the version check can tell you whether the version you're running is the newest one, and the lowest one.
- The Updater's own status plays along. The Updater's own status is the status of its own process: on a machine with no network, the Dashboard shows the Updater's status as "Delta" (a comparison between the installed version and the latest available version), and a direct version check of
/api/statusshows the status directly. - A version is reliably a number. The version is pinned to the VERSION file: the VERSION file says "v0.1.8e", the Updater's version check reads it, and the Dashboard, Deliverator, and every other continuous run of an update check reads the installed version out of the same file.
Two phone fixes
- Dashboards on phones are treated as one, and the updates listen to their dashboard. Dashboard events are consumed in the dashboard, and the updates I do now are more responsive to the dashboard than the previous model was: a dashboard on a phone doesn't react to its own data unless someone taps it, and the mobile-awareness now works as expected.
- The dashboard and the status column now move together. The chat and the status column used to be laid out atop one another, and the status column now sits on its own column on a phone, alongside the chat, not beneath it.
Chips — installs that prove themselves (#79)
- Discovery: chips by repo. Chips are now discovered by repository, with a ranking and a new listing: you can find new chips at the URL given, from a GitHub repo. A chip's README — how it makes its decisions — is pulled from the repo, and its files are installed exactly as the repo lays them out — a package structure, preserved:
chips/— a chip repo becomes a subdirectory here (e.g.chips/my_chip/), and is used from there.chips_v2/— a chip repo becomes a subdirectory here, too (e.g.chips_v2/my_chip/).- A chip researcher can read a chip by typing its name into the search box, and the chip will be installed exactly as the repo lays it out.
- Legacy chips run. The installer now fetches a chip repo's .py files recursively, so package layouts keep their relative paths, and reads
chip-requirements.txtwhen a repo declares one — the card surfaces the declared dependencies plainly ("not installed automatically"; nothing is auto-installed). At run time, a chip with package structure loads as a package member, so its relative and package imports resolve, and every sandbox gets the legacynavi.pycompatibility shim (get_parameters, old semantics) unless the chip ships its own.