Two-Process Kernel Architecture

Design for splitting the notes browser into a browser process (wx GUI) and a separate kernel process (Python executor), communicating over a shared protocol. See kernel-protocol for the message specification.

Motivation

Currently all cell execution runs synchronously in the wx main thread. A long-running cell blocks the entire process: the control socket cannot respond, the UI cannot repaint, and the user sees a frozen window. stdout output only appears after the cell completes — progress updates are invisible during execution.

Process Overview

Two processes replace the single monolithic process:

Browser Process Internals

The KernelClient replaces SheetKernel in sheet_ui.py. It presents the same interface (run_cell(id, source, inputs), reset(), interrupt()) but instead of executing Python locally it sends messages to the kernel process and receives async events back.

Incoming kernel events are dispatched on the wx event loop via wx.CallAfter so the UI updates safely. The control socket thread, the kernel-listener thread, and the wx thread are all distinct — no blocking.

The KernelClient owns a background reader thread that consumes the transport connection and posts events to wx. The reader thread never touches wx directly.

Kernel Process Internals

The kernel process is launched by the browser at startup (pipe mode) or started independently and connected to via Unix domain socket. It consists of:

Cell execution runs in a dedicated thread so the KernelServer can still process control messages (interrupt, status queries) while a cell is running. On receiving execute.interrupt, the kernel raises KeyboardInterrupt into its own executor thread.

Transport Options

Pipe (local, tightly coupled)

The browser spawns the kernel as a child process with subprocess.Popen(stdin=PIPE, stdout=PIPE). Stdin carries browser→kernel messages; stdout carries kernel→browser events. Stderr is forwarded to the browser's own stderr for debugging.

Advantages: zero configuration, no filesystem artefacts, kernel lifetime tied to browser (clean shutdown).

Disadvantages: kernel namespace is lost when the browser restarts; cannot reconnect to an existing kernel; kernel must be on the same machine.

Unix Domain Socket (local, loosely coupled)

The kernel process is started independently and listens on a Unix domain socket (path configured or derived from the notes server URL). The browser connects as a client. No network stack, no authentication needed — access control is via filesystem permissions on the socket file.

Advantages: kernel survives browser restarts (namespace preserved); browser can reconnect after a crash; kernel can be started before the browser or shared between sessions.

Disadvantages: kernel must be started separately; socket file path must be agreed between browser and kernel; stale socket file must be cleaned up on kernel exit.

Commonality Between Transports

The KernelClient and KernelServer are transport-agnostic. Both use a Connection abstraction with two methods:

RS (0x1E, ASCII Record Separator) is used instead of newline as the message terminator. It cannot appear unescaped in valid JSON, making framing unambiguous. See also RFC 7464 (JSON Text Sequences).

Pipe and Unix domain socket provide concrete implementations of this interface. The protocol layer (message types, sequencing, event dispatch) is identical in both cases.

Startup Sequence (Pipe Mode)

Startup Sequence (Unix Domain Socket Mode)

Multiple Kernels

The browser maintains a pool of kernel processes, one per runnable page, up to a configurable maximum (e.g. max_kernels = 4). Each kernel owns its own namespace and socket. When a runnable page is opened, the browser checks whether a kernel is already running for that page key and connects to it; if not, it starts a new one (if the pool limit allows). If the limit is reached, the least-recently-used idle kernel is shut down to make room.

Future: kernels are not required to be Python. Any process that speaks the kernel protocol (kernel-protocol) over a Unix domain socket can act as a kernel — e.g. a Lisp, Lua, or shell kernel. The kernel.info response identifies the language and version; the browser uses this to set syntax highlighting on cell editors. The lang field on each codeblock in the sheet document indicates which kernel language it requires.

Control Socket Compatibility

The existing JSON-RPC control socket (dev/control-api) is unchanged — it remains on the browser process. After the kernel split, sheet.cell.run and sheet.run_all dispatch to the kernel asynchronously and return immediately with {"status": "started"} rather than blocking. A new sheet.cell.wait method blocks until execute.done is received for the given cell and returns its output. (Note: sheet.cell.await was an earlier name — avoid it since await is a Python keyword.)

File Locations

version 3  ·  created 2026-05-31  ·  updated 2026-06-26