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.
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.
Two processes replace the single monolithic process:
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.
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:
exec() in a thread with redirected stdout/stderr and injected builtins (show, plot, HtmlOutput, input). Sends stream/display/done events to KernelServer as they occur.kernel.reset.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.
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.
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.
The KernelClient and KernelServer are transport-agnostic. Both use a Connection abstraction with two methods:
send(msg: dict) -> None — serialise to JSON, append ASCII RS (0x1E) as record terminator, write to channel.recv() -> dict — read until RS, parse JSON (blocking).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.
{"kind": "event", "event": "kernel.ready", "data": {"version": 1}}.{"kind": "request", "method": "kernel.info"} to confirm compatibility.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.
popit3/outlook-to-analysis). The socket path is derived from the page key, e.g. ~/.notes-kernels/<escaped-key>.sock.--max-kernels 4. Setting it to 1 gives the old single-kernel behaviour.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.
kernel.readykernel.info to confirm 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.)
notes-browser/sheet_kernel.py → becomes the kernel process entry point.notes-browser/kernel_client.py → new file, KernelClient (replaces SheetKernel in the browser).notes-browser/kernel_transport.py → new file, Connection ABC + PipeConnection + UnixSocketConnection.notes-browser/sheet_ui.py → SheetKernel references replaced with KernelClient.