Build a checkpointing Python kernel from scratch (greenfield project — only CLAUDE.md exists). The kernel stores immutable execution states (snapshots of variables, modules, timestamps) and creates new states by executing code against existing ones. It exposes a REST API via Bottle + Cheroot for multi-threaded access.
ldakernel/
├── pyproject.toml
└── snapshot_kernel/
├── __init__.py
├── kernel.py # Core kernel logic
└── main.py # Bottle REST API server
Minimal package config with only two external dependencies: bottle and cheroot.
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.backends._legacy:_Backend"
[project]
name = "snapshot-kernel"
version = "0.1.0"
dependencies = ["bottle", "cheroot"]
requires-python = ">=3.9"Simple package marker, exports SnapshotKernel.
This is the core file. All imports are from the standard library (ast, copy, ctypes, datetime, io, sys, threading, traceback, types, uuid).
Stateclass: Holdsname,namespace(dict),timestamp(ISO 8601 string)._snapshot_namespace(namespace)helper: Iterates over namespace dict. Skips dunder keys (__builtins__, etc.). Stores module references directly (modules are singletons, can't be deep-copied). Deep-copies everything else viacopy.deepcopy, falling back to direct reference if that fails.- On
reset()and__init__, an"initial"state is created with an empty namespace.
Since cheroot runs multiple requests in parallel, sys.stdout/sys.stderr are process-global and must not be naively redirected. Solution:
ThreadSafeWriterclass: Wraps the originalsys.stdout/sys.stderr. Usesthreading.local()to store per-threadStringIObuffers. If a buffer exists for the current thread, writes go there; otherwise, writes go to the original stream.- Installed once in
SnapshotKernel.__init__()as replacements forsys.stdoutandsys.stderr. - Each
execute()call sets_thread_local.buffer = StringIO()before execution and reads it after.
Fields:
_states:dict[str, State]— state storage_lock:threading.Lock— protects_states_executions:dict[str, Thread]— mapsexec_idto the thread running it_exec_lock:threading.Lock— protects_executions
Methods:
reset(): Clears_states, creates"initial"state with empty namespace.list_states(): Returns list of state names (under_lock).get_state(state_name): Returns serializable dict withname,timestamp, andvariables(each variable as{type, repr}). ReturnsNoneif not found.delete_state(state_name): Removes state from dict (under_lock).execute(code, exec_id, state_name, new_state_name=None):- Generate
new_state_nameviauuid.uuid4().hexif not provided. - Snapshot the source state's namespace (under
_lock, briefly). - Register
exec_id -> current_threadin_executions. - Set up per-thread stdout/stderr capture via
ThreadSafeWriter. - Parse code with
ast.parse(). If the last statement is anast.Expr, split it off: compile preceding statements asexec, compile last expression asevalto capture its return value (mimicking Jupyter behavior). - Execute via
exec()/eval()on the copied namespace. CatchKeyboardInterrupt(from interrupt) and generalException. - Build Jupyter-style output list:
stream(stdout/stderr),execute_result(last expr value astext/plain),error(exception info with traceback). - On success, snapshot the modified namespace and store as new state. On error, do not store a new state.
- Unregister
exec_id. Return{output, state_name, error}.
- Generate
interrupt(exec_id): Looks up the thread in_executions, usesctypes.pythonapi.PyThreadState_SetAsyncExcto raiseKeyboardInterruptin that thread. This gets caught by theexcept KeyboardInterruptinexecute().
- The
_lockis only held briefly (to read/write the state dict), so multipleexecute()calls run in parallel on independent namespace copies. - Each execution runs in a cheroot worker thread — no need to spawn our own threads.
ThreadSafeWriterensures stdout/stderr capture is isolated per thread.
| Method | Path | Kernel Method | Body (JSON) |
|---|---|---|---|
POST |
/execute |
execute() |
{code, exec_id, state_name, new_state_name?} |
GET |
/states |
list_states() |
— |
GET |
/states/<name> |
get_state() |
— |
DELETE |
/states/<name> |
delete_state() |
— |
POST |
/reset |
reset() |
— |
POST |
/interrupt |
interrupt() |
{exec_id} |
A @app.hook('before_request') checks the token URL parameter against the configured secret. Returns 401 on mismatch.
Custom entry point with argparse:
python -m snapshot_kernel.main --bind 0.0.0.0:8080 --token=SECRETParses --bind (default 127.0.0.1:8080) and --token (required). Runs the Bottle app with server='cheroot'.
copy.deepcopywill fail on some objects (file handles, generators); fallback is to store a reference (shared between states).PyThreadState_SetAsyncExccannot interrupt blocking C extensions (e.g., long numpy ops). This is an inherent Python limitation.- Each state stores a full namespace copy — memory grows with state count and variable size.
- Install the package:
pip install -e . - Start the server:
python -m snapshot_kernel.main --bind 127.0.0.1:8080 --token=test123 - Test basic execution:
curl -X POST 'http://127.0.0.1:8080/execute?token=test123' \ -H 'Content-Type: application/json' \ -d '{"code": "x = 42\nprint(x)", "exec_id": "e1", "state_name": "initial"}'
- Test state listing:
curl 'http://127.0.0.1:8080/states?token=test123' - Test state retrieval:
curl 'http://127.0.0.1:8080/states/<name>?token=test123' - Test chained execution (execute against the state produced in step 3):
curl -X POST 'http://127.0.0.1:8080/execute?token=test123' \ -H 'Content-Type: application/json' \ -d '{"code": "x + 1", "exec_id": "e2", "state_name": "<name_from_step_3>"}'
- Test interrupt: start a long-running execution, then POST to
/interrupt. - Test auth: make a request without token or with wrong token, verify 401.
- Test reset: POST to
/reset, verify states are cleared except newinitial.