Skip to content

UX: shut down orphaned kernel during save-as room migration #389

@rgbkrk

Description

@rgbkrk

Context

After the local-first Automerge migration (#540) and the save-to-daemon delegation (#545, #567), save-as migrates the notebook to a new room based on the file path. The daemon writes the .ipynb, the Tauri relay disconnects from the old room and reconnects to a new room keyed by the new path. However, the kernel stays attached to the old room and gets orphaned.

Current behavior (post-migration)

  1. User has untitled notebook with running kernel (room = UUID-based)
  2. User does Cmd+S → save dialog → picks ~/projects/analysis.ipynb
  3. Daemon writes file via SaveNotebook { path: Some(...) }
  4. Tauri relay disconnects from old room, reconnects to new path-based room
  5. Old room still has the kernel running — no peers, so it schedules eviction (30s timeout via keep_alive_secs)
  6. New room has no kernel — daemon auto-launches a fresh kernel (from prewarmed pool or cold start)
  7. User loses kernel state (variables, imports, etc.)
  8. After 30s, old room evicts and shuts down the orphaned kernel

What the user sees

  • Kernel status briefly shows "Not Started" then flips to "Starting" → "Idle" as the new kernel launches
  • All variables and imports from the previous session are gone
  • If they had a long-running computation, it continues in the orphaned kernel until eviction (but output can't reach the frontend)

Architecture details (current)

Save-as flow (crates/notebook/src/lib.rs save_notebook_as):

  1. Sends NotebookRequest::SaveNotebook { format_cells: true, path: Some(path) } to daemon
  2. Daemon formats cells, writes .ipynb to the new path
  3. Tauri updates context.path, window title, dirty flag
  4. Clears sync handle (disconnects from old room)
  5. Calls initialize_notebook_sync() with new path-derived notebook_id
  6. New room created on daemon, auto-launch triggers fresh kernel

Room eviction (crates/runtimed/src/notebook_sync_server.rs L794-854):

  • When all peers disconnect, schedules eviction check after keep_alive_secs (default 30s)
  • If no peers reconnect, shuts down kernel and removes room

Kernel state: Kernels are tied to rooms via NotebookRoom.kernel. There's no mechanism to migrate a RoomKernel from one room to another.

Recommended approach: Option C (auto-relaunch) with dismissable warning

The cleanest approach for the common case (saving an untitled notebook for the first time):

  1. Before save-as: Check if a kernel is running in the current room
  2. If yes: Show a brief toast: "Kernel will restart in the new location" (dismissable, not blocking)
  3. During save-as: Explicitly shut down the old kernel before disconnecting (don't wait for 30s eviction)
  4. After reconnect: The daemon auto-launches a fresh kernel with the correct working directory (derived from the new file path)

Why not kernel migration (Option B)?

Kernel migration (move kernel from old room to new room) is technically possible but has issues:

  • The kernel's cwd was set at launch time (from the old room's working_dir or the prewarmed pool). Moving it to a path-based room doesn't change the kernel's cwd — relative paths in the notebook would break.
  • The kernel's connection file, ZMQ sockets, and PID are tied to the old room's RoomKernel struct. Moving them requires careful lifecycle management.
  • The daemon's auto_launch_kernel logic in the new room would need to detect "kernel already present" and skip launching — adding complexity.

Implementation sketch

In save_notebook_as (Tauri side):

// Before disconnecting from old room, shut down the kernel explicitly
if let Some(handle) = sync_handle {
    let _ = handle.send_request(NotebookRequest::ShutdownKernel {}).await;
}

This is one line. The kernel shuts down immediately instead of lingering for 30s. The new room's auto-launch gives the user a fresh kernel with the correct cwd.

Toast notification (frontend):

The save callback in useAutomergeNotebook could return metadata about what happened (e.g., { saved: true, kernelRestarted: true }). App.tsx shows a brief toast: "Saved to analysis.ipynb — kernel restarted."

Or simpler: useDaemonKernel already tracks kernelStatus. When it transitions from "idle" → "not started" → "starting" → "idle" during save-as, the toolbar status indicator shows this naturally. The user sees the kernel restart in real time. A toast might be unnecessary.

Edge cases

  • No kernel running: Save-as proceeds without any kernel lifecycle concern. No toast needed.
  • Long-running computation: The explicit shutdown interrupts it. This is the same behavior as "Restart Kernel" — the user should save/checkpoint before save-as if they have important state. A warning dialog ("You have a running kernel. Save As will restart it. Continue?") could be added for this case.
  • Save to same directory: Even if the new path is in the same directory, the room ID changes (UUID → path-based), so the kernel still restarts. This is correct — the kernel cwd should match the file location.
  • Save-as failure: If the daemon save fails, we don't disconnect from the old room, so the kernel stays. No orphan.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions