Skip to content

Latest commit

 

History

History
42 lines (32 loc) · 12.1 KB

uber.md

File metadata and controls

42 lines (32 loc) · 12.1 KB

The uberbot

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 settings uber.bot (its bot id here; "" for none) and uber.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) (a Promo: an error text, or the changes to log; "" demotes). Only a live bot of this machine can be promoted. Laws uber_*.
  • 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 log UberSet with 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 with status (BU.tabs.for, BU.tab.dflt). Actions uber (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 as b:<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 sends setting.set of 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_n the 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". Laws plane_*.

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 uid by UberSet). 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}. origin is the machine, n its own count (1, 2, ...), stamp a hybrid logical clock <secs 10 digits>.<counter 6 digits>.<origin> (string order is time order; Ub.stamp.next never goes behind a stamp seen), op compact JSON (mem, forget, space, page, who, routine, routine-del, fired, lease, bind; builders Ub.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; keys mem:<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.firedby who 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 (law uber_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.mcp and the routine tick call it (Bots.ub). Changes derived from another machine's ops do not go through it. Ub.init logs the first ops of a promoted bot (its name and look, memories, space, page, routines, lease term 1, bind), after the server logs UberSet (UberDo "init").
  • Sync: uber.sync over the signed POST /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's vv says 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> in FR.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 setting uber.offer (JSON {uid, name, look, machine, link}; a setting, not info, so all four clients read it from their state; Ub.offer(st)). RPC bots.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 its bind; 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 (from bind ops and the setting uber.link.<uid>.<machine> logged when an exchange first arrives over a link), Ub.far.named the latter, and BU.rows leaves 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.select uses it for the uberbot and Bots.chat.is keeps 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_GRACE overrides, 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) logs lease{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.tick runs an uberbot routine only where Ub.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 a fired op, 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, over Ub.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. Tests test/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.