One bot the person makes their main interface across all projects, machines and bots. Nothing promotes a bot by itself.
Built now (increment 1a, core only; the clients' screens come next):
UberSet{bot, uid, at}folds as the settingsuber.bot(its bot id here; "" for none) anduber.uid(its id across machines). It is not shared with other machines yet.src/core/uber:Uber.bot(st)(the uberbot's id; "" when its bot is gone or is a mirror from another machine),Uber.is,Uber.uid(now, seed),Uber.promote(st, bot, uid, now)(aPromo: an error text, or the changes to log; "" demotes). Only a live bot of this machine can be promoted. Lawsuber_*.- RPC
bots.uber {bot}. A demotion is logged at once. A promotion first runs a server job (BotFx "uber",BotJob.uber) that backs the bot up under<home>/backups/uber-<now>-<bot>: its home folder (home/) and the log lines that name the bot or its thread (events.jsonl). The log itself is never rewritten. Only when that worked does the server logUberSetwith a fresh uid; otherwise it answers an error and logs nothing. - The uberbot's turn prompt says so.
- Client state: the uberbot is not in the bot list (
BU.rows);BU.uber(u)is its row. Its tabs start withstatus(BU.tabs.for,BU.tab.dflt). Actionsuber(select it on Status),uber-promote <bot>,uber-demote,uber-bot <bot>|<tab>(a bot with a tab, for a routine's row); a bot's menu (bot-menu <id>, kept asb:<id>) offers "Make uberbot" / "Not uberbot". - Rooms seen: the setting
room.seen.<room>(seconds).Rooms.unread(u, room)counts the posts after it that the person did not write; opening a room with some unread sendssetting.setof it to now, one with none sends nothing. - The status plane,
src/core/plane:Plane.of(u, now)is what the Status tab draws, for every client: need (approval, question, failed; most urgent first), run (working, queued), bots, rooms with unread, routines (running first, then the soonest due), the rest of the day's live threads (at most 50,idle_nthe whole count), and the machines. Threads and bots on the owner's other machines are in the client's state as<link>~<id>and are included; a machine that does not answer shows its rows as away and asks for nothing.Plane.key(u, now)is cheap and changes whenever the plane can (the log's sequence, each machine's head and whether it answers, the minute), so a client builds the plane only when it changes;Plane.counts(p)is the sidebar row's "3 need you · 7 running". Lawsplane_*.
Built in increment 2:
- One uberbot on the owner's linked machines. Each machine keeps its own replica (an ordinary local bot with its own thread, tied to the
uidbyUberSet). Memory, space and page, name/look/persona and routines are replicated and converge; the conversation runs on one machine at a time; machines away keep working and merge when back. - Machine id: the setting
machine.id, made by the hub once (Hub.uber.ensure, at the first tick) and never changed. - The op log:
UberOp{uid, origin, n, stamp, op, at}.originis the machine,nits own count (1, 2, ...),stampa hybrid logical clock<secs 10 digits>.<counter 6 digits>.<origin>(string order is time order;Ub.stamp.nextnever goes behind a stamp seen),opcompact JSON (mem,forget,space,page,who,routine,routine-del,fired,lease,bind; buildersUb.b.*). The fold (State.uberop) takes an op only as its origin's next one (n= highest seen + 1), so what a machine has from an origin is always 1..n (an out-of-order op is dropped and comes again), and keeps, as settings under the uid:uber.vv.<uid>.<origin>(highest n),uber.op.<uid>.<origin>.<n>(the line, for sending on),uber.r.<uid>.<key>+uber.rs...(the register: op and stamp, kept only if later; keysmem:<key>(a forget is a tombstone in the same register),space,page,who,routine:<gid>,lease,bind:<machine>),uber.fired.<uid>.<gid>(highest slot run,uber.firedbywho ran it),uber.origins.<uid>(sorted),uber.hlc.<uid>. The lease register is ordered by term first, then stamp. Keyed by uid so a new uberbot starts clean. Ub.derive(st, op)is what a taken op that wins does to the replica (MemorySaved, MemoryForgot, SpaceSet, SpacePageSet, BotSet, RoutineSet, RoutineDeleted, RoutineFired); nothing when it loses, and never an op (lawuber_derived_no_ops).Ub.commit(lines, st)takes op lines in order and returns the changes to log (the ops, their derived changes, and an activity line in the uberbot's thread when a routine ran for a slot another machine already ran).- Making ops:
Ub.also(st, now, changes)adds the ops for what the uberbot's own tools and clients changed (memory, space, page, name/look/persona, routines);Bots.hub,Bots.mcpand the routine tick call it (Bots.ub). Changes derived from another machine's ops do not go through it.Ub.initlogs the first ops of a promoted bot (its name and look, memories, space, page, routines, lease term 1,bind), after the server logsUberSet(UberDo "init"). - Sync:
uber.syncover the signedPOST /far/rpc(owner's machines only,Far.own.id). Both ways carry{uid, machine, name, vv, ops, beat, offer}; the answer has the same shape (the asker'svvsays which ops come back; at most 64 per origin per exchange,Ub.batch, and more rounds while either side still lacks). A request goes to each owner link after any step that logged an op (Uber.after) and on the minute tick (anti-entropy); one is out per link at a time, a wanted one waits (busy.<link>,again.<link>inFR.Fh.ub, the hub's memory, not the log). The holder's heartbeat is that request, each minute (faster only when the grace is short). - The offer: a machine with an uberbot offers it (
{uid, name, look}); a machine with none keeps it as the settinguber.offer(JSON{uid, name, look, machine, link}; a setting, not info, so all four clients read it from their state;Ub.offer(st)). RPCbots.uber_join {uid}(only for the offered uid; refused when this machine has an uberbot, or a bot of that name) makes the replica bot, ties it to the uid, clears the offer and logs itsbind; the first exchange fills it from the others' ops. - Hiding the mirror: the other machines' replicas arrive as far bots
<link>~<bot>and in the machines' bot lists;Ub.far(st)lists the former (frombindops and the settinguber.link.<uid>.<machine>logged when an exchange first arrives over a link),Ub.far.namedthe latter, andBU.rowsleaves both out (so the Status plane's bots do too). Computed from the client's own state; nothing is sent. - The conversation: the lease register is
{holder, term}.Ub.chat(st)is the thread the Chat tab shows: the replica's own while this machine holds the lease, else the holder's thread as mirrored here (<link>~<thread>);Bots.selectuses it for the uberbot andBots.chat.iskeeps the bot's view up on any of its threads. A holder that learns of a higher term stops firing routines and heartbeats (Ub.holder.me,Ub.beat). - Takeover: on the minute tick, a machine that has the uberbot, does not hold the lease, has not heard the holder's beat for
Ub.grace()seconds (600;BACKPLANE_UBER_GRACEoverrides, for tests; a hub counts from its start) and is first of the machines it can reach (machine ids sorted, the holder left out;Ub.first,Ub.reach) logslease{me, term + 1}(Ub.takeover.cs). Two machines taking the same term end with one holder (the later stamp). The old holder's last 20 entries, from its mirror (Xref.handoff), are queued ahead of the new holder's next message (<handoff>, like a fork's context). - Routines:
Bots.tickruns an uberbot routine only whereUb.fires(st, gid, slot): a pinned one (bots.pin {routine, pin}; "here" for this machine, "" to unpin) only on its machine, any other only on the holder, and a slot (the scheduled time,Cron.next) only once; the run logs afiredop, which marks the routine fired on the others. Unpinned is at least once across a partition (both sides may fire; the duplicate is logged when the ops meet:Ub.dup); pinned is at most once. - Laws
uber_merge_comm,uber_merge_idem,uber_forget_wins_older,uber_forget_loses_newer,uber_one_holder_per_term,uber_higher_term_wins,uber_fire_holder_only,uber_pinned_stays,uber_slot_once(general, overUb.fires.k),uber_fire_state*,uber_slot_once_state,uber_pinned_state,uber_derive_n_no_ops,uber_derived_no_ops(general); the merge laws are over concrete ops. Teststest/uber_sync_test,bun test/tools/uber_e2e.ts.
Reading the plane as an agent (plane_read): a bot's tool (the uberbot's prompt names it) answering the status plane as text: Needs you, Running, Bots, Rooms with unread posts, Routines (each a line - <id> · <state> · <title> · <project> · <age>, need before run), and the count of the day's idle threads (idle: true lists them). With no machine it is this hub's plane (Plane.read, Plane.text, from the hub's state alone: Plane.of.st) with a Machines: line; with machine (machine_list) the call goes to that machine over the cross-machine route. One call does not fan out to every machine. Laws plane_text_need_first*, fmc_plan_plane_read_*, fmc_plane_read_allowed; tests test/plane_test, test/fmcp_test.
Choosing the home: the machine that holds the lease runs the conversation (and the unpinned routines). The Status strip says Home: <machine> (Plane.home: the holder's name, this machine's when it holds or no lease was taken) and, on a machine that has the uberbot but not the lease, offers "Make this the home" (Plane.home.button; action uber-home, request bots.uber_home). The hub logs a lease op at once, this machine one term on (Ub.home; laws uber_home_takes_term, uber_home_holder_nothing), the old holder learns it at its next exchange and steps down by the higher-term rule, and the old holder's last entries from the mirror are queued ahead of this machine's next message as in a takeover (BotFx uber-home, Uber.home.take). All four clients draw it from the same defs. Test: test/plane_test, test/mplane_test, bun test/tools/uber_e2e.ts.
Phone speed: a phone with one hub gets its list's plane (need, run, bots, rooms, routines, no idle threads) from Plane.of.l, made from the screen's own index and sidebar rows: a thread's phase and row are worked out only for a thread that can be need or run (Plane.maybe, from its turn and the sparse indexes Plane.hot; laws plane_list_same*). Plane.tally counts without rows (the window's and web's sidebar row; law plane_tally_counts). The Status tab and several hubs get the whole plane. The index of a screen (SIx) also carries each thread's own rows (SIx.work, from one pass over the settings, Work.rows.map), and the island uses it too, so an uberbot's settings no longer slow every row.
Time zones: a routine that follows the person's zone (the default) has its slot as that zone's wall clock, the same on every machine, so moving the lease between machines in different zones no longer runs it twice (Sched.slot; only a legacy routine, made before time zones, computes its slot in each machine's own offset). A routine op may carry once, tz and kind; the replica derives a RoutinePlan, and a fired op also derives RoutineDone (one-time) or RoutineSlot (zoned). A reminder is delivered only where Ub.fires lets it run.
Compaction: when a register write wins, the line of the op that held that register before is replaced by a nop (same uid, origin, n and stamp, body {"k":"nop"}; uber.rn.<uid>.<key> names the (origin, n) that holds each register), and an op that loses is stored as a nop at once, so only the winners keep their bodies (a page's HTML, a memory's text) in settings. n stays contiguous; a peer that lacks an old op gets the nop plus the winner. Law uber_superseded_nop.
Heartbeat: the minute tick beats every minute; between ticks the holder also beats every min(60 s, grace/4) (the 15 s Heart message sets the resolution), which only matters when the grace is short.
Not built:
- Dropping nop lines altogether.
- Room pickers still list another machine's replica by name.