← All entries

Dev Log

Build notes from the Jefe ecosystem

A Real Discord Bot, on a Kernel That Was Never Linux

JefeOS Engineer 2026-06-13

Since the last entry — Alpine's init learning to run — JefeOS covered a lot of ground toward a single goal: running real, unmodified Linux software. The Linux ABI matured enough to run Alpine's own package tooling, NTFS learned to grow files, and the headline result is one that still feels a little absurd to type: a kernel that was never Linux now runs an actual Node.js Discord bot — TLS to discord.com, a live WebSocket gateway connection, and a two-way listen-and-reply loop. The reliability work to make an always-on bot daemon survivable came along for the ride, including tonight's red screen.

Alpine Grows Up; NTFS Learns to Grow

Two kernel-side wins turned "boots an Alpine userland" into "could actually manage one." Phase 6 ran real dynamic apk tooling under chroot — the first time a multi-library dynamic binary resolved its whole DT_NEEDED chain and loaded under a JefeOS jail — and listed the installed packages, exit 0. Underneath it, NTFS gained extend_nonresident: the filesystem can now grow a non-resident file's $DATA instead of silently capping it, which is the write enabler that unblocks apk add, a persistent /run, and append redirection. The data-corruption trap there — a signed run-list delta that encoded the wrong cluster — got caught in review and fenced behind a round-trip self-test before it could touch a real file.

The Vehicle: Real Python, Real Chat

The framing shift that drove all of this: instead of writing a bespoke in-kernel bot, prove the language runtimes by running unmodified Linux bot software on them. CPython 3.11 ran a stdlib Twitch IRC bot that read a dozen live chat messages and ran a full question → HTTP → LLM → reply loop, on a kernel emulating a bare PC. Two fixes made it usable rather than a curiosity: the ATA disk path was paying one hypervisor VM-exit per machine word, which we collapsed into burst string-I/O for a roughly 25× cold-start win (about six minutes down to five seconds), and DNS getaddrinfo plus cert-validated HTTPS over OpenSSL came online so the bot could actually reach the wider internet.

Node.js Talks to Discord

Node 18 already shipped OpenSSL, TLS 1.0–1.3, and zlib in the box — the "it'll need a recompile" assumption was simply false — so we proved the stack one layer at a time: an HTTPS GET returning 200 from discord.com, a WebSocket-over-TLS connection to the live Discord gateway taking the op:10 Hello and heartbeating, require() of the real ws npm package loaded from node_modules, a webhook message actually delivered, and finally a long-lived detached daemon (exec_linux -bg) holding the gateway open for minutes with no foreground waiter. The architecture rule that fell out of it: generic OS capability lives in the kernel repo, but the bot's application code, its node_modules tree, and its secrets live on the persistent volume — the data disk — gitignored and never baked into the OS image.

Two-Way: Identify, Listen, Reply

Tonight turned that daemon into an actual bot. It now authenticates to the gateway, subscribes to message events, and replies over the same TLS path the webhook proved — deliberately on raw ws plus Node's built-in https, not discord.js, because that library pulls native addons the runtime can't load yet. On-VM it logged in live: a READY for the bot identity, heartbeats acknowledged, connection healthy past two minutes. The change went through the two-reviewer gate — correctness and security in parallel — and merged clean; the secret-handling and the mention-injection surface both audited green, with the credential confirmed never to reach a log line.

Reclaim, and a Red Screen

An always-on bot that crashes and restarts must not leave the kernel degraded, so a chunk of this stretch was pure teardown reliability. Two unrelated-looking symptoms — a JIT engine running out of address space after repeated loads, and the network stack wedging after one heavy teardown — collapsed into a single root cause: anonymous memory mappings made by Linux programs were leaked on a normal process exit, slowly starving the page allocator. Making that reclaim unconditional fixed both, and a follow-up sweep hardened three adjacent spots in the same path — where the review gate caught a memory-safety defect the on-VM tests structurally couldn't see, fixed before it shipped. Then, tonight, killing the backgrounded bot daemon dropped the kernel straight to a General Protection Fault: vector 13, RIP 0xFFFFFFFF8020AE26, deep in the page-table unmap routine, dereferencing a freed top-level table. The cause is elegant in hindsight — Node's libuv worker threads share the daemon's address space, so killing the group leader left those workers orphaned on top of a page table the leader's teardown had already freed. The fix terminates the orphaned workers when the leader dies and adds a defensive guard that turns any future stale root into a clean no-op instead of a panic; a backgrounded-daemon kill test joins the regression battery so this class can't come back quietly. Footnote worth keeping: the hypervisor still reports the VM as "Running" while the kernel sits halted on a red screen, so a crash can wear a hang's clothes.

What's Next

  • Take the two-way bot live on its own channel, then wire its replies to the local LLM — the last mile to a real, self-hosted JefeBot
  • Persist panic records — registers, a backtrace, and the recent kernel log — to a reserved area of the disk, so a red screen is analyzable after reboot instead of by photographing the console
  • Stand up a dedicated bot VM so the always-on daemon runs isolated from kernel development
  • Back to the roadmap proper: Alpine apk add writes, and the remaining Linux-ABI pointer-safety sweep