Τιθωνός · persistent sessions for remote Jupyter
Tithon keeps a Jupyter kernel — and every byte it ever printed — alive on the host, independent of any client. Close your laptop mid-run; reopen hours later over SSH or a VSCode tunnel. The output is still there, and still streaming.
● alpha — in daily use · rough edges · bug reports genuinely useful
Ἠώς, goddess of the dawn, asked Zeus to make her lover immortal — and forgot to ask that he stay young. Tithonus lived forever, withering. A remote kernel bears the same curse: the process outlives your disconnect, but everything it says is lost the moment you leave.
TITHON LIFTS THE CURSE — IMMORTALITY, WITH THE ETERNAL YOUTH THIS TIME.
( AND IT RHYMES WITH PYTHON. )
Reconnects, but everything printed while you were away is gone. iopub output streams over the WebSocket and is never persisted server-side — there is nothing to replay.
Ties the kernel to the extension-host process. Close the window or drop the network and the kernel dies, taking the whole session — hours of state — with it.
Survives the disconnect, but you lose rich output — plots, HTML, widgets — and you can't open the same session from a second client.
The root cause is the same in all three: the source of truth lives on the client. Tithon moves it to the host.
The kernel is plain ipykernel — Tithon replaces the session-management layer around it, not the execution engine. A long-lived daemon journals everything the kernel says and serves it back to any client, at any time, from any point in the stream. Full design in docs/SPEC.md.
setsidKill −9 the daemon, upgrade it, restart it — the kernel keeps computing and re-attaches through a persisted connection file. The daemon is disposable; your state is not.
sqlite · WALEvery iopub/shell message is persisted exactly as the kernel sent it, alongside a folded per-execution snapshot — so reconnects restore the current display without replaying history.
attach(last_seen_seq)Clients resume from the last sequence they saw: one snapshot, then an ordered, gapless delta stream. Reconnecting isn't a recovery procedure — it's just resuming.
.tithon/outputs/Images are never base64-embedded. They're files referenced by hash and GC'd down to what the current display needs — a live plot converges to O(1) disk.
widget-state+jsonipywidgets traffic folds into a state mirror. A tqdm bar or a slider is restored at its real, current value — not a stale frame from when you left.
TITHON_SUB_QUEUE_MAXPer-subscriber buffers are capped; a client that falls too far behind is dropped and resyncs on reconnect. One slow reader can't grow daemon memory or block the rest.
State lives under ~/.tithon — socket, log, journal, artifacts. Start the daemon, run something, then kill the daemon mid-flight. Watch nothing happen to your kernel.
$ pip install tithon $ tithon daemon & $ tithon run -c 'x = 41' $ tithon run -c 'x += 1; print(x)' 42 # now prove the point — kill the daemon. $ pkill -9 -f 'tithon daemon' # the daemon dies… $ tithon daemon & # …the kernel does not $ tithon attach --since 0 --once # full snapshot: it's all back $ tithon run -c 'print(x)' 42 # state intact
code --install-extension rnoro.tithon — or search “tithon” in the
Extensions view.
A plain percent-format script (# %% cells) opens as a real notebook —
same cells, same Run buttons, same rich output. The file stays pure source; outputs
never touch it, so diffs stay clean.
Selecting the kernel attaches the session. Reopen the notebook tomorrow — output, progress and widgets are restored and resume streaming, no command needed.
An .ipynb buries fifty lines of code in two hundred fifty lines of JSON, then base64-encodes the plots into it. Tithon keeps source as source: outputs live in the journal, images are real files — ones a coding agent can hand to a model as actual images it can see, not tokens it burns and still can't read.
{
"cells": [
{
"cell_type": "code",
"execution_count": 3,
"metadata": {"collapsed": false},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAoAAAAHg
CAYAAAA10dzkAAAAOXRFWHRTb2Z0d2FyZQBNYXRw
bG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8v
…41 more lines of base64…",
"text/plain": ["<Figure size 640x480>"]
},
"output_type": "display_data"
}
],
"source": ["for epoch in trange(48):\n", …
# %% load df = pd.read_parquet("runs/07-04.parquet") # %% train for epoch in trange(48): loss = step(model) # %% plot plt.plot(history["loss"]) # outputs → journal · images → .tithon/outputs/ # the .py stays pure source. diffs stay clean.
Don't feed your LLM idiot JSON.