Something went wrong. Try again.
a tiny pet with a GPT-2 brain, living in your browser
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550import type { HostingMode, PetCommand, PetState, RemoteSnapshot } from "../types.ts";import { playEffect } from "./animation.ts";import { BRAINS, loadBrain, stopBrain } from "./brain.ts";import { CFG } from "./config.ts";import { setState } from "./engine.ts";import { startAcceptedGame, stopGameControls } from "./game.ts";import { timeZone } from "./save-format.ts";import { catchUp, resetSimulationClock } from "./simulation.ts";import { parseSave, saveNow } from "./storage.ts";import { brain, emit, store, toast, ui, view } from "./store.ts";import { errorMessage } from "./utils.ts";
const CONNECTION_KEY = "tokenling.remote.v1";const STOPS_KEY = "tokenling.remote-stops.v1";interface PendingStop { base: string; id: string; key: string; deviceId: string; epoch: number; operationId: string; activation?: boolean;}interface Connection { base: string; id: string; key: string; epoch: number; deviceId: string; created: number; activation?: { epoch: number; operationId: string }; release?: { epoch: number; operationId: string };}let timer = 0;let synchronizing = false;let stopping = false;let stopTimer = 0;let lastEffect = 0;let commanded = false;let seen: { id: string; epoch: number; revision: number } | null = null;let initializedEffects = false;export const isRemote = () => store.state.hosting.mode === "remote";export const canRunLocally = () => !isRemote() && !ui.hostingBusy;
function connection(): Connection | null { try { const raw = localStorage.getItem(CONNECTION_KEY); if (!raw) return null; const value: Connection = JSON.parse(raw); return value.created === store.state.created ? value : null; } catch { return null; }}function keepConnection(value: Connection) { localStorage.setItem(CONNECTION_KEY, JSON.stringify(value)); ui.remoteEndpoint = value.base;}function pendingStops(): PendingStop[] { const raw = localStorage.getItem(STOPS_KEY); return raw ? JSON.parse(raw) : [];}function keepStops(stops: PendingStop[]) { if (stops.length) localStorage.setItem(STOPS_KEY, JSON.stringify(stops)); else localStorage.removeItem(STOPS_KEY);}function queueStop(c: Connection) { if (!c.release) return; const stops = pendingStops(); if (!stops.some((s) => s.base === c.base && s.id === c.id)) { stops.push({ base: c.base, id: c.id, key: c.key, deviceId: c.deviceId, ...c.release, activation: !!c.activation, }); keepStops(stops); }}export function stopPendingStops() { clearTimeout(stopTimer); stopTimer = 0;}export function normalizeEndpoint(value: string) { const url = new URL(value); if ( url.username || url.password || url.search || url.hash || !["http:", "https:"].includes(url.protocol) ) throw new Error("Enter the server's web address, without a password, query or fragment."); if (url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) throw new Error("Use an HTTPS server address."); return url.href.replace(/\/+$/, "");}async function request<T>( base: string, path: string, key?: string, body?: unknown, method?: string,): Promise<T> { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 15000); try { const response = await fetch(`${base}/api/${path}`, { method: method || (body === undefined ? "GET" : "POST"), headers: { ...(key ? { Authorization: `Bearer ${key}` } : {}), ...(body !== undefined ? { "Content-Type": "application/json" } : {}), }, body: body === undefined ? undefined : JSON.stringify(body), cache: "no-store", credentials: "omit", signal: controller.signal, }); const result = await response.json(); if (!response.ok) throw new Error(result.error || `The server returned HTTP ${response.status}.`); return result; } finally { clearTimeout(timeout); }}// The host runs the same engine; its effects play exactly like local ones.function applyEffects(snapshot: RemoteSnapshot) { for (const e of snapshot.effects) { if (e.seq <= lastEffect) continue; lastEffect = e.seq; playEffect(e); }}function adopt(snapshot: RemoteSnapshot, c: Connection) { if (!store.tabOwned || store.badEgg || store.resetting) return; const current = connection(); if (current?.id === c.id && snapshot.epoch < current.epoch) return; if ( seen?.id === c.id && (snapshot.epoch < seen.epoch || (snapshot.epoch === seen.epoch && snapshot.revision < seen.revision)) ) return; const next = parseSave(JSON.stringify(snapshot.state)); if (!BRAINS.some((b) => b.id === snapshot.modelId)) throw new Error( "This page cannot run the server's selected brain. Keep your current recovery save and update the page.", ); c.epoch = snapshot.epoch; keepConnection(c); const devicePreferences = store.state.settings; next.settings = { ...devicePreferences, chatter: next.settings.chatter }; next.hosting.brainId = snapshot.modelId; // Only the destination device may resume automatically after a lost handoff acknowledgement. const handedHere = snapshot.authority === "local" && snapshot.localDevice === c.deviceId; next.hosting.mode = handedHere ? "local" : "remote"; setState(next); seen = { id: c.id, epoch: snapshot.epoch, revision: snapshot.revision }; ui.remoteWeather = snapshot.weather; ui.remoteLastSync = Date.now(); ui.remoteError = snapshot.authority === "local" && !handedHere ? "Your pet has moved to another device. This is its last server checkpoint." : snapshot.lastError && !snapshot.thinking ? "The server's brain could not finish its turn. It will retry shortly." : ""; if (!snapshot.game && ui.remoteGameId && view.game) { view.game = null; view.target = view.x; } ui.remoteGameId = snapshot.game?.id || null; Object.assign(brain, { id: snapshot.modelId, state: ui.remoteError ? "off" : "ready", busy: snapshot.thinking, remoteFeatures: snapshot.features, }); view.thinking = snapshot.thinking; if (!initializedEffects) { lastEffect = snapshot.effects.at(-1)?.seq || 0; initializedEffects = true; } applyEffects(snapshot); if (snapshot.game && !ui.hostingBusy) startAcceptedGame(); saveNow(); emit(); if (handedHere) { stopRemote(); stopBrain(); resetSimulationClock(); catchUp(); void loadBrain(snapshot.modelId); }}export function stopRemote() { clearTimeout(timer); timer = 0;}async function syncRemote() { if (!isRemote() || !store.tabOwned || ui.hostingBusy) return; if (synchronizing) { timer = setTimeout(syncRemote, 250); return; } const c = connection(); if (!c) { ui.remoteError = "This browser no longer has the server access key. You can recover locally from its saved checkpoint."; brain.state = "error"; emit(); return; } synchronizing = true; const execution = store.executionRevision; try { let snapshot = await request<RemoteSnapshot>(c.base, `pets/${c.id}`, c.key); if (execution !== store.executionRevision || !isRemote() || ui.hostingBusy) return; if (c.activation && snapshot.authority === "local" && snapshot.epoch === c.activation.epoch) { snapshot = await request<RemoteSnapshot>(c.base, `pets/${c.id}/transfer`, c.key, { ...c.activation, mode: "remote", deviceId: c.deviceId, state: store.state, }); } if (execution !== store.executionRevision || !isRemote() || ui.hostingBusy) return; delete c.activation; adopt(snapshot, c); } catch (error) { if (execution !== store.executionRevision || !isRemote()) return; ui.remoteError = `Could not reach your pet's server: ${errorMessage(error)}`; Object.assign(brain, { state: "error", error: ui.remoteError, busy: false }); view.thinking = false; emit(); } finally { synchronizing = false; if (execution === store.executionRevision && isRemote() && store.tabOwned) timer = setTimeout(syncRemote, document.hidden ? 15000 : 1000); }}export async function resumeRemote() { stopRemote(); ui.remoteEndpoint = connection()?.base || CFG.remoteEndpoint || ""; await syncRemote();}export async function remoteCommand(command: PetCommand) { if (!isRemote() || !store.tabOwned || ui.hostingBusy || commanded) return false; const c = connection(); if (!c) { toast("The server access key is missing. Bring your pet back to local mode to recover it."); return false; } commanded = true; const execution = store.executionRevision; try { const snapshot = await request<RemoteSnapshot>(c.base, `pets/${c.id}/commands`, c.key, { epoch: c.epoch, operationId: crypto.randomUUID(), command, }); if (isRemote() && execution === store.executionRevision) adopt(snapshot, c); return true; } catch (error) { toast(errorMessage(error)); return false; } finally { commanded = false; }}function beginMove() { if (!store.tabOwned || store.badEgg || store.resetting || ui.hostingBusy) throw new Error("This tab cannot move the pet right now."); if (view.game) throw new Error("Finish the current game before moving your pet."); ui.hostingBusy = true; store.executionRevision++; stopRemote(); stopGameControls(); stopBrain(); view.thinking = false; emit();}export async function moveToRemote(address: string) { if (ui.hostingBusy || isRemote()) return; const previous = structuredClone(store.state); const hadBrain = !!brain.worker; let activationSent = false; try { const base = normalizeEndpoint(address); beginMove(); const modelId = previous.hosting.brainId || CFG.modelId; const health = await request<{ protocol: number; modelId: string; cpu: boolean }>( base, "health", ); if (health.protocol !== 1 || !health.cpu || health.modelId !== modelId) throw new Error("That server cannot run your pet's selected brain. Your local save is safe."); let c = connection(); if (c?.release) queueStop(c); let prepared: RemoteSnapshot | null = null; if (c && c.base === base) { if (c.release) { try { const released = await request<RemoteSnapshot>(c.base, `pets/${c.id}/transfer`, c.key, { ...c.release, mode: "local", deviceId: c.deviceId, state: null, }); c.epoch = released.epoch; delete c.release; keepConnection(c); keepStops(pendingStops().filter((s) => s.base !== c!.base || s.id !== c!.id)); } catch { c = null; } } if (c) { try { prepared = await request<RemoteSnapshot>(base, `pets/${c.id}`, c.key); if (prepared.authority !== "local") c = null; } catch { c = null; } } } else c = null; if (!c) { const key = [...crypto.getRandomValues(new Uint8Array(32))] .map((b) => b.toString(16).padStart(2, "0")) .join(""); c = { base, id: crypto.randomUUID(), key, deviceId: crypto.randomUUID(), epoch: 0, created: previous.created, }; keepConnection(c); prepared = await request<RemoteSnapshot>(base, "pets", c.key, { id: c.id, state: previous, modelId, }); } c.epoch = prepared!.epoch; c.activation = { epoch: c.epoch, operationId: crypto.randomUUID() }; keepConnection(c); // The host keeps the pet's clock in this device's current time zone. setState({ ...store.state, hosting: { mode: "remote", brainId: modelId, timeZone: timeZone() }, }); saveNow(); // durable intent comes before activation, so a reload cannot run two copies activationSent = true; const snapshot = await request<RemoteSnapshot>(base, `pets/${c.id}/transfer`, c.key, { ...c.activation, mode: "remote", deviceId: c.deviceId, state: store.state, }); delete c.activation; lastEffect = snapshot.effects.at(-1)?.seq || 0; initializedEffects = true; adopt(snapshot, c); toast(`${store.state.pet?.name || "Your egg"} is now living on your server.`); } catch (error) { // A timed-out activation may have succeeded. Its persisted remote intent must be reconciled first. if (!activationSent) { setState(!previous.pet && !hadBrain ? { ...previous, egg: null } : previous); resetSimulationClock(); if (previous.pet || hadBrain) void loadBrain(previous.hosting.brainId || CFG.modelId); } ui.remoteError = errorMessage(error); toast(ui.remoteError); } finally { ui.hostingBusy = false; emit(); if (isRemote()) void resumeRemote(); else catchUp(); void releaseRecoveredCopy(); }}export async function moveToLocal(recover = false) { if (!isRemote() || ui.hostingBusy) return; const c = connection(); const preferences = store.state.settings; try { beginMove(); if (!recover) { if (!c) throw new Error("The access key is missing. Use the local recovery copy instead."); const status = await request<RemoteSnapshot>(c.base, `pets/${c.id}`, c.key); let snapshot = status; if (status.authority === "remote") { c.epoch = status.epoch; if (c.release?.epoch !== c.epoch) c.release = { epoch: c.epoch, operationId: crypto.randomUUID() }; keepConnection(c); snapshot = await request<RemoteSnapshot>(c.base, `pets/${c.id}/transfer`, c.key, { ...c.release, mode: "local", deviceId: c.deviceId, state: null, }); } else if (status.localDevice && status.localDevice !== c.deviceId) { throw new Error( "Your pet has moved to another device. You can make a local recovery copy from this browser's checkpoint.", ); } if (status.authority === "local") { // Fence a prepared record too: an older activation may still be in flight. c.epoch = status.epoch; if (c.release?.epoch !== c.epoch) c.release = { epoch: c.epoch, operationId: crypto.randomUUID() }; keepConnection(c); snapshot = await request<RemoteSnapshot>(c.base, `pets/${c.id}/transfer`, c.key, { ...c.release, mode: "local", deviceId: c.deviceId, state: null, }); } setState(parseSave(JSON.stringify(snapshot.state))); c.epoch = snapshot.epoch; delete c.activation; delete c.release; keepConnection(c); keepStops(pendingStops().filter((s) => s.base !== c.base || s.id !== c.id)); } else if (c) { c.release ||= { epoch: c.epoch, operationId: crypto.randomUUID() }; keepConnection(c); queueStop(c); } setState({ ...store.state, hosting: { ...store.state.hosting, mode: "local" }, settings: { ...preferences, chatter: store.state.settings.chatter }, }); brain.remoteFeatures = undefined; ui.remoteGameId = null; ui.remoteError = ""; view.game = null; saveNow(); resetSimulationClock(); void loadBrain(store.state.hosting.brainId || CFG.modelId); toast( recover ? "Continuing locally from this browser's last checkpoint. The server's copy will be paused when it reconnects." : "Your pet is back on this device, with its memories and diary.", ); if (recover) void releaseRecoveredCopy(); } catch (error) { ui.remoteError = errorMessage(error); toast(`Could not move your pet: ${ui.remoteError}`); } finally { ui.hostingBusy = false; emit(); if (isRemote()) void resumeRemote(); else catchUp(); }}export async function releaseRecoveredCopy() { if (!store.tabOwned || store.resetting || stopping) return; stopPendingStops(); stopping = true; try { const c = connection(); if (!isRemote() && c?.release) queueStop(c); for (const stop of pendingStops()) { if (!store.tabOwned || store.resetting) break; try { const status = await request<RemoteSnapshot>(stop.base, `pets/${stop.id}`, stop.key); if (!store.tabOwned || store.resetting) break; // A lost activation acknowledgement is the only permitted epoch adjustment. if (stop.activation && status.epoch === stop.epoch + 1 && status.authority === "remote") { stop.epoch = status.epoch; stop.operationId = crypto.randomUUID(); stop.activation = false; keepStops( pendingStops().map((s) => (s.base === stop.base && s.id === stop.id ? stop : s)), ); } let epoch = status.epoch; if (!(status.authority === "local" && status.epoch > stop.epoch)) { const snapshot = await request<RemoteSnapshot>( stop.base, `pets/${stop.id}/transfer`, stop.key, { epoch: stop.epoch, operationId: stop.operationId, mode: "local", deviceId: stop.deviceId, state: null, }, ); epoch = snapshot.epoch; } if (!store.tabOwned || store.resetting) break; keepStops(pendingStops().filter((s) => s.base !== stop.base || s.id !== stop.id)); const current = connection(); if ( current?.base === stop.base && current.id === stop.id && current.release && !isRemote() ) { current.epoch = epoch; delete current.release; delete current.activation; keepConnection(current); } // A stop receipt never replaces state on a newer local or remote branch. } catch { /* retain the key and receipt even when this pet moves to another host */ } } } catch (error) { ui.remoteError = `Could not read the pending server stop: ${errorMessage(error)}`; emit(); } finally { stopping = false; try { if (store.tabOwned && !store.resetting && pendingStops().length) stopTimer = setTimeout(releaseRecoveredCopy, 30000); } catch { /* retain an unreadable stop receipt instead of overwriting its key */ } }}export async function deleteRemote() { const c = connection(); if (isRemote() && !c) throw new Error("The access key is missing. Recover locally before starting over."); if (isRemote() && c) await request(c.base, `pets/${c.id}`, c.key, undefined, "DELETE"); stopRemote(); localStorage.removeItem(CONNECTION_KEY);}export function recoverySave(): PetState { const copy = structuredClone(store.state); copy.hosting.mode = "local"; return copy;}export async function changeHosting(mode: HostingMode, address: string) { if (mode === "remote") await moveToRemote(address); else await moveToLocal();}