Skip to content

wisp 0.0.6

Latest

Choose a tag to compare

@vcealicu vcealicu released this 10 Oct 00:10
· 24 commits to main since this release

LocalGhost wisp 0.0.6

Cut on 10 October 2026. The sixth cut of wisp; the name is the release line, the number the
cut. Everything in 0.0.1's notes still holds, and the pages for 0.0.2,
0.0.3, 0.0.4 and 0.0.5 say what each cut changed.

A phone on 0.0.5 renews its certificate at its first unlock on 0.0.6 and gets a two-week one;
the box names it by its public key from then on and moves its records over once. The weather
list refills over about two hours after the redeploy. sudo ./tools/redeploy.sh lays update.sh
and the geo tools under /opt/localghost, which the INTEGRATIONS page's fetches need, and hands
the Wikipedia tables to the daemon at the next unlock.

What changed since 0.0.5

The heights in packs

The mirror's elevation set was one GeoTIFF a degree square, some 26,000 files for the world: the
manifest listed every one (megabytes the phone and every box read, and what hid the server
release from the phone in 0.0.4), a fetch was thousands of requests, a folder of them slow to
list. The set carries packs now, one a 30-degree block (GLO-90_N30_W030.heights is 30 to 60
north, 30 west to 0; some sixty files for the world, a gigabyte or two each), each the block's
tiles with their bytes as they were behind a small index, so the box's GeoTIFF reader reads a
tile out of its section of the pack (internal/dem/pack.go). ghost-heights pack <tiles> <out>
makes them for the mirror's publish (in the release bundle, a setup tool; the same tiles give the
same bytes), list and check look inside one. fetch_geo.sh picks the packs whose block
touches the box asked for (GHOST_GEO_ELEVATION="34:72,-25:45") and keeps reading loose tiles
where a box has them; a folder may hold both. ghost.framed's elevation says how many packs it
read beside the tiles.

The Wikipedia import, faster and honest

Measured on xyntai on 5 October: 15,000 entries a minute, 22 hours for the English edition, where
the notes said an hour or two. Most of it was the five lookup indexes (pattern, trigram, text)
kept current through twenty million inserts. They are the store's now, not the schema's: taken
off when an import starts, made over the whole once it is done (minutes), and seen to at every
start with a finished import. The slice is 55 seconds of every minute, and it reads with several
readers at once (half the cores, two to four): the entries are cut into as many ranges, each
reader has the file open for itself (its own cluster cache) and a connection of its own, and the
state keeps every reader's place, so a stop resumes each where it was; a state from before goes on
as one reader. The state says how long is left at the pace so far, in the log, in ghost-cli ghost.synthd wiki (leftMinutes, readers), in Box Status ("about 3 hours to go"), on the
WIKIPEDIA page and in health.sh. ghost-cli ghost.synthd wiki again=1 starts the import over.

The first import on xyntai stopped at a page with a stray Latin-1 byte in it ("invalid byte
sequence for encoding UTF8", a batch the database refused, tried again every minute from the
same place). Every title and text is made valid UTF-8 before it goes in (a bad byte becomes the
replacement character, a NUL goes), and a batch the database still refuses is written a row at a
time with the refused rows dropped and counted, so one bad page never stops Wikipedia.

The Postgres extensions, checked

tools/pg_extensions.sh says whether vector (pgvector) and pg_trgm (the likeness lookups) are
in the OS tree and, on an unlocked box, in the runtime bundled onto the volume. setup.sh runs
install_db.sh when one is missing, server_setup_root.sh prints the line, redeploy.sh checks both
trees first and says what to do (the redeploy goes on: the box runs without an extension, the
lookups that want it do not), and health.sh says when the likeness lookups are off.

One memory per person, through the ones you edited

A person's memory edited by hand took no part in the merging, so a name corrected by hand left
the corrected row beside the rows the box kept adding ("Cristina is my wife", four times). The
edited row is the one kept now: its words stand, its spelling of the name is the title, the
others' names become its other names (the misspelling among them, so later mentions land on it),
their facts fold in, and they go. A distilled memory titled with a person's name ("Cristina ·
Cristina is Vlad's wife") folds into that person's memory as a fact and goes too; a capitalised
title alone makes no person.

The people's names, spelt your way, in every transcript

Whisper wrote a name as it has seen it written most ("Christina"). Every transcription is told
the people's names first (whisper's initial prompt, from the memories: every person's name and
other names, the owner's too) and read once after: a capitalised word that sounds like a known
name (Christina, Kristina for Cristina; Tobi for Toby), or is one letter off a longer one, becomes
the name as the memories spell it, a possessive kept. A word that is not a name, a near name of
someone else (Tony beside Toby), a name already right: left alone.

WIKIPEDIA on the phone: stats, and a search that works while the import runs

The page says what the box has: the edition, the counts, the size in the database, when it was
imported and how long it took, what was skipped, what the search can do (by title and prefix
always; the words of a lead and close spellings once the indexes are built), and how many chat
questions the article answered since the box started. The title indexes stay through an import
now (only the heavy ones wait for its end), so a search by title finds what is in already while
the file is still going in, and the chat's exact lookups stay quick; the lookups by the words of
a lead and by likeness are off until the indexes exist, rather than reading the whole table.

The chat shows how it got there

While an answer is on its way the status line now stacks: the steps so far dim above the one
under way ("asked your box what to look for", "sent 'ferry Corfu Albania' to Brave", "3 found, 2
read on this phone"). The box adds its own in the context event (steps: the memories read, the
photos matched, the article from its Wikipedia, the archive matches, the day's news, its own
numbers, the web findings and how they were ranked). After the answer the trail stays under it
behind how (n steps). A reopened chat has no trail yet: the box keeps the answer and its
sources, not the steps.

The card abroad: the word, with the prices and the news beside it

Away from home the widget and the lock-screen card led with the phrase and nothing else. The
phrase stays first; the widget's top line now ends with BTC and ETH ("› français · 12/80 · BTC
65,000 · ETH 3,250"), and the card pulled open adds the prices with their change and the day's
top story under the phrase's own lines. The box's brief is fetched wherever the phone is.

The check-in page, tidied

The strip shows the weekday under each day. The why and the recorder share one card; a line
under it says what goes in ("today: tired, calm · why · a voice note of 0:42"); SAVE CHECK-IN is
a button, lit only when there is something to save. The day checked in is a card of its own, with
the day's mark, the feelings, the why and every note said to it.

SOURCES: what the box draws on, in one list, fetched from the app

The menu had NEWS, CRYPTO and WIKIPEDIA as three doors and no way to see what the box held or to
add to it without a shell. They sit under one page now, SOURCES: a row each for Wikipedia, the
news, the market numbers, the weather, the maps and the heights, and speech, with its state (●
ready, ◐ partial or importing, ○ missing, ✕ off), a line saying what the box has ("6,712,345
articles, 2024-12 edition", "12 feeds, 11 answered in the last day", "no place list: the weather
has nothing to pull"), and a page to open where there is one. A row the box lacks offers a fetch:
one tap asks what it is and what it costs, a second starts it, and the page follows it while it
runs (GET /v1/sources, POST /v1/sources/fetch {step}; secd runs the same tools/update.sh
the operator runs by hand, as root, one at a time, the log under /var/lib/ghost/update/jobs/).
Wikipedia's fetch is the 50 GB file and the import after it; the maps' heights are asked for by
region, a box of the world around the phone's last fix (the position goes to the box and nowhere
else). redeploy.sh and the release bundle put update.sh, the setup scripts and the geo tools
under /opt/localghost/ so secd finds them.

The news feeds, kept from the app

SOURCES › News › feeds lists the publications the box reads with how each is doing (when it last
answered, how many entries, failures), switches one off or on with a tap, takes one off the list
with a second tap, and adds one by its address (GET/POST /v1/news/feeds, which drive synthd's
news add/remove/enable). A feed added is fetched within two hours, sooner on HOME's
pull-to-refresh. The old instruction on SETTINGS (ghost-cli ghost.synthd news add=…) is gone.

The weather on HOME, with where it comes from

A card on HOME shows the forecast nearest the phone's last position: a word for the sky, the
temperature now, the day's high and low, and under it the provenance: the hour of the box's pull,
how many places it holds and how far the nearest is ("pulled 06:10 · 3,000 places · Corfu Town,
4 km"). Without a place list the card says so and points at SOURCES › Maps. GET /v1/weather?lat &lon on secd asks tallyd; the snapshot says when it has no places (noGeo).

Every panel says what it is made of

An ⓘ on each section opens a sheet in plain words: where the data comes from, how it is made,
how often, and what leaves the phone or the box for it. The prices (every exchange's own ticker,
the blend, CRYPTO50's weights and chaining, Coinbase's listing and the ECB's rates), the news
(the feeds, the grouping into stories, the brief), Wikipedia, the weather (Open-Meteo asked for
the world's largest places every morning, never for where you are), the maps and the heights,
the memories, the check-in, health, the gallery, the sources, and speech. The texts are one
object (ui/Explain.kt), tested, so they stay in one place.

HOME's ask box behaves

A question on HOME continued the open chat sometimes and started a new one other times, with
no way to tell which. It continues the chat touched in the last twenty minutes and says so under
the box ("continuing: what did I do in Rome? · new chat"), else starts a fresh one; a tap on
"new chat" starts one on purpose. The box has a microphone now, the same as CHAT's: a question
spoken is heard on the box and sent as text.

A pass over the whole app

Every page was read with its states in mind, and the cheap wrongs put right:

  • Back goes where you came from: the system key and the ‹ in the bar take one path (a page
    opened from SOURCES returns there, a memory to what opened it, the feeds to SOURCES), the
    memory page's ‹ names the page it returns to, an open article on WIKIPEDIA and a past
    check-in close before the page does.
  • Nothing spins forever: HEALTH, MEMORIES, the memory page, WIKIPEDIA, the voice notes, the
    gallery, the chat list and Box Status's drill-in say "the box did not answer , is it
    unlocked?" when it does not, instead of "reading…" for good; the gallery no longer reads a
    failed load as an empty archive.
  • Nothing fails silently: a memory added, edited or deleted, a note jotted, the About note saved,
    a Wikipedia search or article that came back with nothing all say so, and an editor whose
    save did not land stays open with the words in it. The About note can no longer be saved over
    with a blank when the box did not give it first.
  • Nothing destructive is one tap: DEPLOY, ROLL BACK and LOCK BOX NOW ask once in a dialog that
    says what happens; a notification's ✕, a model's DELETE, a connector's DISCONNECT and a recent
    chat's ✕ in the drawer arm on the first tap and act on the second.
  • The words: SETTINGS' verify button says what it verifies (this app's build, not the box's);
    the glossary's "Change code = wipe" was wrong since changepin arrived (it keeps the data;
    resetup is the wipe, for a lost code); "export (not built yet)" became what is true (the
    originals on the volume, the sealed nightly backups); the chat's empty page explains what it
    answers from and its examples are asks, a tap sends one; the model picker says "on this
    phone" rather than "tap to download"; the enrol page says why its button waits (the pairing
    code only ever arrives with a scan); the scan's arrival line says what the box said rather
    than READY before it has answered.
  • The glyphs: one chevron (›) for "opens", one ellipsis (…), ✕ for delete, ◍ for a voice note;
    the emoji are gone.

Still to do from the same pass, recorded in tools/DEV_UPDATE.md: the comma-as-dash in some
ninety strings, the "!" on lines that are not errors, UK dates throughout, "(s)" plurals, and
label case.

Three things from the phone, 9 October evening

The widget abroad ran its head line out of room ("› evening · germany · Deutsch · 0/36 · BTC
82,830 · ETH 2…"): the head keeps the slot, the country, the language and the count, and the
box's brief sits in a foot line under the buttons (the prices with their changes, the day's top
story on the line after). The card pulled open puts the prices and the story straight after the
word, before "next" and "then", and ends with one short count ("0/36 known · level 1 survival ·
1/25"): the OS cuts a long big text at the bottom, so the least wanted line is the one that goes.

A Wikipedia the box held in full was reported as "none on the box" and unread by the chat when
its import state had gone (wiki again=1 after the import had finished, or the settings row
lost): the state is rebuilt from the tables and the .imported marker (wiki.Store.Recover),
on the next tick of the import loop and whenever the status is asked. The page's "none" line
points at SOURCES now, and a state it does not know is shown as the box said it.

Box Status's tallyd line listed every coin's price ("AAVE 171.47/6 ADA 0.24/6 …", a hundred of
them); it says the count and the spread of venues and the three everyone looks for ("118 coins
blended from 1 to 6 venues each (4 from one) · BTC 82,753 · ETH 2,491 · SOL 109.58"). The
whole list is ghost-cli ghost.tallyd rates and the COIN pages.

The weather for far more of the world, at a walking pace

The list was the three thousand largest places, a hundred thousand people or more, which
crowded into the countries with the biggest cities and left Neu-Ulm with no pulled place within
120 km. The world is cut into half-degree cells now (55 km a side), each cell keeps its largest
town of fifteen thousand people or more, and the six thousand largest of those cells are the
list: the same count reaches most of the inhabited world, since towns in one cell share a sky.
The pull is a hundred places every two minutes, the longest unpulled first, a place pulled
again once its row is a day old, so a day's pull stays under Open-Meteo's free limits on the
sternest count (every place a call, six thousand times 1.3 a day, 130 a batch); the order is the
rows' age and nothing else, never where the person is. A row older than 36 hours no longer
answers. weather fetch=1 pulls the next batch; the ctl says the pace and the counts since
start; Box Status and SOURCES say the new list.

The Wikipedia tables are the daemon's

The import's index drop and make failed on xyntai with "must be owner of table wiki_articles":
Postgres gives CREATE INDEX to a table's owner alone, and every table was the owner role's while
the daemons connect as ghost_rw. wiki_articles and wiki_redirects are handed to the
read-write role at unlock (DaemonOwnedTables, as the superuser, after the converge, the
read-only role keeping SELECT, the owner role every privilege with the grant option so its
blanket grants still go through, and the read-write role CREATE on the schema, which an index
needs), and left out of the ownership converge. synthd tries the indexes
again every minute until they are made and clears the error from the state when they are; the
WIKIPEDIA page shows a ready state's lingering error under its line.

WIKIPEDIA on the phone: the page had never been given the answer

secd's /v1/wiki wrote the ctl answer's text where synthd's wiki command answers in its
data, so the phone got an empty body for every ask and showed "none on the box" over a full
Wikipedia, with searches that found nothing. The handler writes the data now (the text where a
command answers that way, as onthisday does), and the page works: the state and the stats,
the search, the article.

INTEGRATIONS: a card each, a page each with the pull drawn

SOURCES is INTEGRATIONS: a card each (Wikipedia, the news, the market numbers, the weather, the
maps and the heights, speech) with its glyph, its state, one line of what the box has and how
many places it draws from. A card opens the integration's page: at the top the pull is drawn,
the places it draws from on the left, the box on the right, the data running the lines between
them while the page asks the box and breathing once it has answered, a place that is off
sending nothing; then what the box has, the places one row each with its mark and what it
gives (the feeds with their entries and failures, the exchanges with their answer rates and
pairs, Coinbase's listing and the ECB, Open-Meteo and GeoNames, OpenStreetMap, Geofabrik,
Copernicus and the mirror, whisper.cpp and its model), the actions (open the page, the feeds, a
fetch from the mirror behind a question), and how it is made, folded. GET /v1/sources carries
each integration's from rows.

The screen, now and then

The app is a terminal on a small green screen, a Pip-Boy with a journal in it, and the journal
and the assistant are one thing, so the look stays one voice: no second palette for your words,
no theme to switch. The glass shows itself the way the unlock's rings and the scanner's aperture
already do, as a moment rather than a coat of paint: once in a while, on a page change, the
screen does one small thing for a second or two (a wash of scanlines with a vignette that fades
in and out, a bright line that sweeps down the page revealing the new one beneath it, a heading
that types itself in) and is plain again. Never twice inside a minute and a half, and one page
change in four after that, so a session sees one every few minutes at most. All drawing, no
touch taken; SETTINGS › SCREEN ("the odd flicker") turns it off. The memories list carries
edited (a memory you wrote or rewrote) for the pages to use.

A phone's certificate lasts two weeks and renews while the phone is used

A device certificate was good for ten years: a phone enrolled once held the door for a decade,
and a phone lost or lent kept it until someone retired it by hand. Now a certificate is good for
two weeks and the phone renews it once a day while it talks to the box: at an unlock, or from
the background notification poll, once the certificate it presents is a day old. The renewal
re-signs the same key, the one the phone made in its secure element at the first unlock and
that has never left it; the old certificate is retired when the phone confirms over the new. A
phone in use never sees the end of its certificate; a phone left for two weeks presents one the
box's door refuses, the gate says so before the PIN is tried ("this phone's key ran out … scan
the box's QR to enrol it again"), and the re-enrol row is right there. SETTINGS › YOUR BOX says
when the key was made and how long it is good for.

The QR's own certificate, the one whose key was on the terminal, is good for an hour: scan,
unlock once with the PIN, and the phone's own key replaces it; after the hour a QR never scanned,
or photographed, reaches nothing, and ghost-qr draws a fresh one.

The box names a phone by its public key now (the SHA-256 of the certificate's SPKI, sixteen hex),
not by the certificate's hash: a renewal is the same phone, nothing moves, and ghost-cli ghost.secd retire by device key holds across renewals. A phone filed under its old name is
moved over once, the first time it is seen with the volume up. secd checks a certificate's dates
itself on every request rather than lean on nginx (the header it passes over 127.0.0.1 is
trusted until the edge passthrough), and the rekey proof is bound to the certificate presented
(v2), so a proof taken from one phone is no proof from another. The retired list keeps each
retired certificate's expiry beside it and forgets it once past; a retirement by hand is kept
for good.

The widget, tall

A widget given three launcher rows or more shows the extra lines under the buttons: the
weather where the phone last was (from the box's own table, the phone's last fix going to the
box and nowhere else, kept with the brief every quarter hour), and what comes next, the next
two phrases abroad or the next two stories at home. Resizing the widget on the launcher
redraws it; a short one shows none of this.

Known gaps at 0.0.6

  • A fresh box trusts the device-certificate header nginx passes over 127.0.0.1 until the operator
    runs ghost-ctl edge-passthrough --domain <box name>; a box reached by IP alone cannot take the
    passthrough yet (nginx routes it by name), which is next.
  • The box CA key stays on the OS disk, by choice: a phone can be enrolled and refused while the
    vault is locked.
  • A phone left for two weeks has to be enrolled again with a fresh QR; carrying its data over
    to the new enrolment (by the continuity key) is next.
  • Releases are signed by the site key alone; an offline release key is planned.
  • The APK is not byte-reproducible; the server set is.
  • The weather is on HOME and in the chat; the lock-screen card is next.
  • CRYPTO50's day value against its live value showed a fault on 3 October whose cause is still
    to be named (ghost-cli ghost.tallyd rates index=1 lays the terms out); the guard holds the
    index meanwhile.
  • SOURCES runs update.sh from the app for Wikipedia, the maps and the heights, and the speech
    engine and model, one at a time; the streets' build is still the operator's by hand. A .deb is not built; the bundle
    and install.sh get a box up without one.
  • NEAR YOU, what you photograph and on this day are still sections of MEMORIES; a page each is next.
  • Mail, the Mist (peer-to-peer backup) and the decoy volume are not built.