Something went wrong. Try again.
llm chat done the right way™
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246import { watch } from "node:fs";import { ASSET_CACHE, buildClient, type ClientBundle, HTML_CACHE, serveAsset } from "./src/assets";import { clientMetadata, handleCallback, handleLogin, handleLogout } from "./src/auth";import { createBlobStore } from "./src/blobs";import { BLOB_GC_GRACE_MS, BLOB_GC_INTERVAL_MS, INPUT_DELTA_TTL_MS, REAP_INTERVAL_MS,} from "./src/config";import { JobDriver } from "./src/drive";import { sweepInputDeltas, sweepOrphanBlobs } from "./src/gc";import { apiRoutes, evictIdleActors, getActor } from "./src/http";import { initInference } from "./src/inference";import { handleLardCallback, handleLardConnect } from "./src/lard";import { sandboxRoutes } from "./src/sandbox";import { getConfig } from "./src/settings";import { shareRoutes } from "./src/share";import { Store } from "./src/store";
/** * Web entrypoint. The client is built explicitly (see src/assets — content-hashed, * compressed, immutable-cached) and served alongside the framework-free API routes * from src/http. Tests import `apiRoutes` directly and never touch this file, so * importing it never triggers a frontend build. * * The inline drive loop shares its implementation with worker.ts via JobDriver; * both entrypoints run the same claim/run/checkpoint logic. */if (import.meta.main) { // Load the catalog + provider registry before serving, so the first request // already has models resolvable. await initInference();
const store = new Store(); const blobs = createBlobStore(); const driver = new JobDriver(store, (id) => getActor(id, store), blobs);
setInterval(() => { void driver.driveOnce(); }, 1000);
// Reaper: re-queue jobs whose lease expired (worker died mid-run) so any // process can re-claim them from checkpoint_seq; also evict idle actors. setInterval(() => { store.reap(Date.now()); evictIdleActors(); store.sweepSessions(); }, REAP_INTERVAL_MS);
// Blob GC: reclaim orphaned blobs (no conversation references them) past the // grace window. Runs on its own slower cadence since it touches the byte store. setInterval(() => { void sweepOrphanBlobs(store, blobs, BLOB_GC_GRACE_MS).catch((err) => console.warn(`blob gc: ${(err as Error).message}`), ); // The same idea one level down: the log keeps what was said, not the // keystrokes it arrived in. const dropped = sweepInputDeltas(store, INPUT_DELTA_TTL_MS); if (dropped > 0) console.log(`[gc] dropped ${dropped} spent argument fragments`); }, BLOB_GC_INTERVAL_MS);
// The favicon <link>s and the manifest are bundled straight from the HTML // heads (Bun hashes them and rewrites the hrefs). The manifest's own icons // and the OG image, though, are referenced only by absolute path (manifest // JSON and og:image/twitter:image meta), so the bundler never sees them — // serve those from ./public at the paths they name. const publicDir = new URL("./public/", import.meta.url); const file = (name: string) => () => new Response(Bun.file(new URL(name, publicDir))); const staticRoutes = { "/icon-192.png": file("icon-192.png"), "/icon-512.png": file("icon-512.png"), "/icon-512-maskable.png": file("icon-512-maskable.png"), "/og-image.png": file("og-image.png"), };
// KaTeX's stylesheet + fonts, served straight from the installed package (no // vendoring, no CDN). The client injects the <link> at runtime on first math, // so the bundler never resolves it and nothing math-related loads until used. // The CSS references its fonts relatively, resolving under /vendor/fonts/. const katexDir = new URL("./node_modules/katex/dist/", import.meta.url); const vendorRoutes = { "/vendor/katex.min.css": () => new Response(Bun.file(new URL("katex.min.css", katexDir)), { headers: { "Content-Type": "text/css", "Cache-Control": "public, max-age=31536000, immutable", }, }), "/vendor/fonts/:name": (req: Bun.BunRequest<"/vendor/fonts/:name">) => { // Only a bare KaTeX font filename — never a path segment that could escape. if (!/^KaTeX_[\w-]+\.(woff2|woff|ttf)$/.test(req.params.name)) { return new Response("not found", { status: 404 }); } return new Response(Bun.file(new URL("fonts/" + req.params.name, katexDir)), { headers: { "Cache-Control": "public, max-age=31536000, immutable" }, }); }, };
// The enrichment bundle (Shiki + KaTeX) is heavy and optional, so it must NOT // ride the app entry. Bun.serve's HTML bundler inlines dynamic imports, so we // build enrich.js as its own SPLIT bundle here (splitting DOES work in // Bun.build) and serve its outputs from /assets/. The client loads /assets/ // enrich.js via a runtime URL, so the app entry never pulls these libs and // grammars load per-language only when a code/math block appears. const enrichAssets = new Map<string, Bun.BuildArtifact>(); async function buildEnrich() { const build = await Bun.build({ entrypoints: [new URL("./src/client/enrich.js", import.meta.url).pathname], target: "browser", splitting: true, minify: true, }); enrichAssets.clear(); for (const out of build.outputs) enrichAssets.set(out.path.replace(/^\.?\//, ""), out); } await buildEnrich();
// The app client (index + the satellite pages), built explicitly so we control // caching, compression, and asset paths (see src/assets). Held in a mutable ref // so the dev watcher can swap in a rebuild without re-registering routes. const dev = Bun.env.NODE_ENV !== "production"; let client: ClientBundle = await buildClient(dev);
// `bun --watch` restarts on server.ts + its imports, but the client and enrich // are built from paths (never imported), so their edits wouldn't otherwise // rebuild. In dev, watch the client sources and rebuild in place; the no-cache // HTML header then lets a plain browser reload pick up the new hashed assets. if (dev) { const dir = new URL("./src/client/", import.meta.url).pathname; let timer: ReturnType<typeof setTimeout> | null = null; watch(dir, { recursive: true }, (_evt, file) => { if (file && !/\.(js|css|html)$/.test(file)) return; // ignore editor temp files if (timer) clearTimeout(timer); timer = setTimeout(() => { Promise.all([ buildEnrich(), buildClient(dev).then((c) => { client = c; }), ]) .then(() => console.log("[client] rebuilt")) .catch((err) => console.error("[client] rebuild failed:", err)); }, 80); }); } const assetRoutes = { "/assets/:file": (req: Bun.BunRequest<"/assets/:file">) => { const file = req.params.file; const a = enrichAssets.get(file); if (!a) return new Response("not found", { status: 404 }); // Chunks are content-hashed → immutable, cached forever. The entry // (enrich.js) keeps a fixed name but its content hash rides an ETag: the // browser revalidates each load (no-cache) but gets a tiny 304 when it // hasn't changed, and only re-downloads the body when it actually did — so // rebuilds propagate without re-fetching the bundle on every page load. const etag = `"${a.hash}"`; const cache = /^chunk-/.test(file) ? "public, max-age=31536000, immutable" : "no-cache"; if (req.headers.get("If-None-Match") === etag) { return new Response(null, { status: 304, headers: { ETag: etag, "Cache-Control": cache } }); } return new Response(a, { headers: { "Content-Type": a.type || "text/javascript", "Cache-Control": cache, ETag: etag, }, }); }, };
const port = getConfig().server.port; // The SSE stream is intentionally long-lived and can sit idle between // generations. Bun's default idleTimeout is 10s — shorter than our 15s // keepalive — so an idle stream would be killed before the first keepalive // fires. Raise it to Bun's max (255s); the keepalive resets the idle clock // well inside that window, so streams stay open indefinitely. // An entry document, served with revalidate-caching. Reads from the mutable // `client` ref so a dev rebuild is reflected without re-registering the route. const page = (name: string) => (req: Request) => { const doc = client.html.get(name); if (!doc) return new Response("client not built", { status: 500 }); return serveAsset(req, doc, HTML_CACHE); };
Bun.serve({ port, idleTimeout: 255, development: dev, // Content-hashed client assets (/chunk-*.js, /favicon-*, …) live at the root // under names that change each build, so they can't be static route keys; // this fallback serves them immutably from the current bundle by basename. fetch(req) { const name = new URL(req.url).pathname.slice(1); const asset = client.assets.get(name); if (asset) return serveAsset(req, asset, ASSET_CACHE); return new Response("not found", { status: 404 }); }, routes: { // The service worker: root scope, revalidated each load so updates land. "/sw.js": () => new Response(Bun.file(new URL("./src/client/sw.js", import.meta.url)), { headers: { "Content-Type": "text/javascript; charset=utf-8", "Cache-Control": "no-cache", "Service-Worker-Allowed": "/", }, }), "/": page("index.html"), "/c/:id": page("index.html"), // deep link to a conversation — the SPA reads the id from the path "/settings": page("index.html"), // SPA route — the shell mounts the settings view "/conversations": page("index.html"), // SPA route — the shell mounts the conversations view "/projects": page("index.html"), // SPA route — the shell's router mounts the projects view "/p/:id": page("index.html"), // SPA route — the shell mounts the project-detail view // A published document, for anyone with the link. Its own document, not // the SPA: a reader here has no session and must never be bounced to a // login for something their link already entitles them to. "/s/:token": page("share.html"), // Auth (indiko OAuth). Inert unless auth.enabled — the SPA only navigates // here after a 401. /client-metadata.json is the public client document. "/client-metadata.json": () => clientMetadata(), "/auth/login": (req: Request) => handleLogin(req), "/auth/callback": (req: Request) => handleCallback(req, store), "/auth/logout": (req: Request) => handleLogout(req, store), // Per-user lard link: auth-code + PKCE to lard's (shared) authorization server. "/lard/connect": (req: Request) => handleLardConnect(req, store), "/lard/callback": (req: Request) => handleLardCallback(req, store), // Public read paths for published documents. Registered here rather than // inside apiRoutes because everything there is wrapped by the auth gate. ...shareRoutes({ store, blobs }), // An HTML artifact, served as a live page from an origin that owns // nothing — the only place one can be given a camera without being given // the app. Inert until server.artifactOrigin names a host (see sandbox.ts). ...sandboxRoutes({ store, blobs }), ...staticRoutes, ...vendorRoutes, ...assetRoutes, // `kick` claims a just-enqueued job immediately instead of waiting for the // 1s poll tick below; the poll remains as a fallback (and for the worker). ...apiRoutes({ store, blobs, kick: () => void driver.driveOnce() }), }, }); console.log(`kloe listening on http://localhost:${port}`);}