Location
Location
Location
LifeOS knows where the principal is from their iPhone, and from any of their Macs running Seat when the phone goes quiet. Every location-aware part of the system reads that one answer. ISA: LIFEOS/LOCATION/ISA.md (not in the public release).
How it works
iPhone (Overland) ─┐
Mac (Seat) ────────┴→ Location Worker + D1 → sync-location (every 5 min) → USER/LOCATION/current.json → here()
- The phone. Overland, an open-source tracker, records location in the background and posts batches of GeoJSON points with a write-only bearer. It queues offline and sends when it can. It is the primary source.
- A Mac. Seat, the menu bar presence app, adds the Mac’s own CoreLocation fix (Wi-Fi positioning, so coarser than the phone) to the presence post it already sends every two minutes, once the principal allows Location for it. Share Location in its menu turns this off. The Worker keeps these fixes in their own
device_pointstable under the Mac’s label, and/latestreturns the newest fix from any device, so a Mac fix only wins once the phone has gone quiet; on a tie the phone wins.current.jsonsourcesays which device it came from (overlandor the label), andLocation.ts nowsays “from the laptop” when it is not the phone. The statusline header shows the fix’s age once it is over 30 minutes old, and STALE past the stale window. - A desktop at home. A desktop Mac (no internal battery in
pmset -g batt) never leaves home, so when the phone’s fix is past the stale window (or the current fix is a plane’s, since he cannot be on a flight and at his desk at once) and that Mac’s keyboard was used in the last 10 minutes, the sync writes a fix at home stamped now (sourcethis-mac). Everything place-aware (header, clock zone, weather, tides, watches) follows it within a minute. The phone’s own fresh fix replaces it as soon as one arrives, and the stale alert and the receiving check still judge the phone alone, so a quiet phone still texts. - The Worker (
LIFEOS/LOCATION/worker, workers.dev, own D1).POST /overlandtakes the write bearer and answers{"result":"ok"}, which is what Overland needs before it drops a batch.GET /latesttakes the read bearer and returns the newest fix by device time within an accuracy limit (3 km by default), so a coarse cell-tower fix never displaces a better recent one. Bearers are stored as hashes with a scope, so a leaked phone token can write but never read. No SSID, device id or unique id is stored. A daily cron deletes points older than seven days. - The sync (
Location.ts sync, thesync-locationworkflow). Reads the read bearer from the login Keychain, pulls the newest fix, and writesUSER/LOCATION/current.json(mode 0600, atomic, git-ignored). That file holds the exact point, accuracy, fix time, city, region, country, timezone, distance from home and an at-home flag. City and zone come from Nominatim and Open-Meteo at two decimals, and only when the fix has moved more than 5 km. - The reader (
Here.ts).here(home)returns where he is, rounded to two decimals. A fix older than the stale window (6 hours by default) still gives the last known place, markedstale: truewith its age, andprincipalTimezone()keeps that place’s zone. Only a missing or corrupt fix gives the home withfallback: trueandat_home: null. It never touches the network and never throws.
Who reads it
| Reader | What follows him |
|---|---|
Every prompt (hooks/lib/presence-context.ts) | When the principal is not at this Mac, a <principal-reach> block names the principal’s device, place and reachFor() channels, so anything the principal must act on reaches them as a share link, a tailnet Pulse URL, the real public URL or a text, never as “do it on the desktop”. Silent at this Mac. Composed by HookBridge prompt-context, or TimeContext when the plugin is not loaded. |
principalTimezone() | The zone of a fresh fix wins over the configured zone; LIFEOS_TIMEZONE still wins over both. |
Quiet hours (TOOLS/QuietHours.ts) | The window is read in principalTimezone(), so on a trip it follows his local clock. |
Watches (loadPlaces().here) | Weather alerts, local quakes and near fire check his point as well as home when he is away. The darksky, ISS and aurora watches compute for his point. Home-bound watches (tides, power-utility outages, city notices, wastewater, highway incidents, aircraft, concerts) stay at the house. |
| Public profile (the optional daemon-profile skill, if installed) | Only on the principal’s command, never on a schedule. Inside the home area the profile shows its default. Away, it shows the region, or the city when he asks. A public venue he names shows for six hours, and only if it is within 2 km of his fix. |
Optional neighbours
Here.ts, Location.ts and the Worker import nothing outside this folder except the shared path and invariant helpers, so they work on an install without the rest (pair also loads TOOLS/TextFormat.ts and calls the Relay bridge’s send.ts at run time). Vigil events, the sync heartbeat and the watches’ home point load at run time when those parts are present. With no watches, the home point comes from home ({ "lat", "lon", "timezone" }) in USER/LOCATION/config.json. A test (ISC-51) fails if a static import into a private part comes back.
Guards
- Jumps. A fix that implies more than 300 m/s from the last accepted one is held until a second fix within 5 km confirms it. A six-hour flight is not a jump.
- Timezone flips. The zone in
current.jsonchanges only after two consecutive syncs agree on it. - The receiving check (
Location.ts receiving, thelocation-receivingworkflow, every 10 minutes). It watches the service from outside the sync, because a stopped sync cannot report its own silence. It raisesstalewhen the newest fix is older thanstale_hours, andsync_downwhen the sync has not writtenstate.jsonfor 10 minutes. Both arehighevents. It keeps its own record inreceiving.jsonand never writes the sync’s files. Itsstaleshares the sync’s cooldown, so a quiet phone texts once, whichever one notices first. - Vigil events (source
location). The phone going stale is ahighevent, so it reaches the principal as a text; its threshold isstale_hours. The phone going stale, coming back, the Worker failing three syncs in a row, and the read bearer being refused are separate events, and each fires at most once per 12 hours. No event or log line carries a coordinate past two decimals.
Commands
bun LIFEOS/LOCATION/Location.ts now [--json] # "City, Region, 4 minutes ago, away from home, 3900 km out."
bun LIFEOS/LOCATION/Location.ts sync # what the workflow runs
bun LIFEOS/LOCATION/Location.ts health # the Worker's /health
bun LIFEOS/LOCATION/Location.ts pair # new phone bearer; texts your own line a one-time setup link
bun LIFEOS/LOCATION/Location.ts rotate read # new Mac bearer into the Keychain; old one revoked
bun LIFEOS/LOCATION/Location.ts rotate write # new phone bearer; Overland setup link to the clipboard, never stdout
Setting up the phone
- Install Overland from the App Store and allow location Always.
- Set
d1_toolinUSER/LOCATION/config.jsonto a CLI that runs as<tool> d1 query <database-id> --sql <sql>(no such tool ships with LifeOS; without itpairandrotatestop and name the setting), then runLocation.ts pair. It mints a phone bearer and texts the principal’s own line a one-time link (/setup/<code>, single use, 15 minutes). Opening it on the iPhone redirects to Overland’s setup scheme, which fills in the URL and token. The bearer itself never enters a message. (rotate writedoes the same through the clipboard when the phone is next to the Mac.) - In Overland, set Continuous Tracking Mode to Both (significant changes plus visits), Desired Accuracy to 100 m, and Send Interval to 5 minutes. Turn tracking on.
Lost phone or leaked token
Run Location.ts rotate write (the old phone bearer stops working at once) and, if the Mac is in doubt, rotate read. The trail on the Worker ages out within seven days.
