Skip to content

feat: sleep a session to free its resources - #212

Open
kaminskypavel wants to merge 2 commits into
AgentSystemLabs:mainfrom
kaminskypavel:feat/sleep-session
Open

kaminskypavel wants to merge 2 commits into
AgentSystemLabs:mainfrom
kaminskypavel:feat/sleep-session

Conversation

@kaminskypavel

Copy link
Copy Markdown
Contributor
z on an idle session asks first Answered: the card stays, its process is gone
The grid with two idle sessions in main and the Sleep agent confirm over it: "Sleep 'agent-2'? Its process ends to free memory; attaching wakes it with the conversation." The same grid after confirming: both cards still listed, the footer reads "'agent-2' is asleep; attach wakes it"

A session can now be put to sleep: z (or Sleep in a live card's right-click menu) ends its agent process to free the memory and CPU it holds, and keeps the card where it is. The IDLE REAPER never takes a session in the worktree you are viewing, so with six sessions on main none of them was ever freed short of archiving; Sleep frees them without losing the card.

Contents

✨ What you get

🧹 Free a session without archiving it

  • Sleep a card: z, or Sleep in the card's right-click menu
    • Ends the agent CLI and the MCP servers it started
    • The card stays in the grid, not archived; it reads as a cold card
    • Rebindable as sleep in Settings › Hotkeys
  • Wake it by opening it: Enter, a click, or resting the cursor on it
    • The agent RESUMES its conversation, exactly as after an IDLE REAP
  • What stayed the same: the IDLE REAPER and session_idle_timeout are untouched; Sleep is the manual version of what the reaper already does

🛡️ Never cut a turn short

  • Always asks first: the CONFIRM DIALOG d and a use
    • Enter/y sleeps, Esc/n keeps it running
  • Refused while working: running, waiting on you, or a backgrounded tool call still going
    • The footer says 'name' is still working; sleep it once it is idle
    • The DAEMON refuses too, on the IDLE REAPER's own test, so a stale TUI view cannot slip past
  • Nothing to sleep: a cold, archived or Claude Cloud card says so; the menu offers Sleep on live cards only

🧭 How it flows

sequenceDiagram
  actor U as User
  participant T as TUI
  participant D as DAEMON
  participant C as agent CLI
  U->>T: z on a card
  alt Running / NeedsFeedback, cold, archived or cloud
    T-->>U: footer says why, nothing sent
  else idle and live
    T->>U: Sleep agent? confirm
    U->>T: Enter / y
    T->>T: release the pane if it shows the card
    T->>D: SleepAgent { id }
    alt busy (status or detached job)
      D-->>T: Error "still working"
    else idle
      D->>C: kill_session
      D-->>T: EntityUpserted (alive: false)
    end
  end
  U->>T: later: attach the card
  T->>D: Attach
  D->>C: respawn with resume
Loading

⚠️ Risk

🎯 Attack surface

  • New ClientRequest::SleepAgent. Reached only by a client already connected to the DAEMON's 0700 socket, the same callers as ArchiveAgent and DeleteAgent, which already kill the same process. Held by: the existing socket auth; the request only kills a session nebula itself spawned and keeps the row. Not held: nothing new; it is strictly weaker than DeleteAgent.

Verdict: 🟢 Low risk: one additive request that reuses the archive and reaper kill path, refuses busy agents, and touches no hot path.

Level Why
🔒 Security & production Low no new reach: same socket, same kill_session the reaper, Archive and Delete already call
⚡ Performance Low off every hot path: runs once per key press; the busy probe is the reaper's detached_job_under, run on a cloned session outside the lock
🧩 Fit with the codebase Low mirrors Archive end to end: MenuAction, PendingAction, CONFIRM DIALOG, keymap entry, protocol-gated request like CheckWorktree

Rollback: git revert removes the key, the menu row and the request. PROTOCOL VERSION goes back to 51; a TUI built from this branch then refuses to send Sleep to that daemon (it is gated), so nothing breaks either way.

🔧 Technical overview

  • Mechanism. Daemon::sleep_agent reads the row, refuses an archived one and a busy one (Running / NeedsFeedback, or agent_has_detached_job on the live PTY, the IDLE REAPER's test), then kill_session and broadcast_agent, so every client sees alive: false. Waking is the existing reaped-session path: the next Attach boots the CLI with its resume flag. The TUI checks the same conditions first for a precise footer, opens the confirm, and on Enter detaches the pane if it shows the card (so nothing re-attaches it straight back), then sends the request.
  • Files. crates/nebula-daemon/src/registry/lifecycle.rs: sleep_agent; crates/nebula-daemon/src/server.rs: the request handler; crates/nebula-core/src/protocol.rs: SleepAgent, PROTOCOL VERSION 52, SLEEP_AGENT_PROTOCOL; crates/nebula-tui/src/event_loop/actions.rs: sleep_agent, sleep_agent_now, confirm_sleep_agent, the menu row; crates/nebula-tui/src/keymap.rs: z; docs/keys.md: the key's row.
  • Not done. Sleeping a running agent behind a "cut it short" confirm: an interrupted turn plus killed background jobs is a worse surprise than waiting for it to go idle. Sleep for terminals: a shell has no conversation to resume, d already closes one.
  • Gate. cargo fmt --check clean, cargo clippy --workspace --all-targets clean, cargo test -p nebula-core -p nebula-daemon -p nebula-tui --lib 1,795 passed, cargo test -p nebula (e2e) 121 passed. New tests: sleep_kills_an_idle_process_and_keeps_the_row (daemon, real PTY) and z_asks_then_sleeps_an_idle_card_and_refuses_a_running_one (TUI). branch_switch::tests::stash_and_switch_leaves_the_changes_in_a_named_stash failed once in a full parallel run and passes alone and with --test-threads=4; it does not touch this code.

📝 Notes

  • Based on upstream/main at 5c361c7 (v0.46.0 + 2); no conflicts.
  • Protocol 51 → 52 is additive (MIN_COMPATIBLE_PROTOCOL stays 49): an older DAEMON keeps working with this TUI, and z there says to restart the daemon. nebula reload moves a running daemon onto the new binary with its sessions.
  • Two SCREENSHOT HARNESS scenes ship with it: make shot SCENE=sleep-confirm and SCENE=sleep-after.

🤖 Generated with Claude Code

`z`, or Sleep in a live card's right-click menu, ends the agent's
process (the CLI and the MCP servers it started) and keeps its card.
Attaching it again resumes the conversation, as after an idle reap.
The idle reaper never takes a session in the worktree being viewed, so
this is the way to free one of those.

It always asks first. A working agent (running, waiting on the user, or
with a backgrounded tool call still going) is refused, by the TUI and
again by the daemon on the reaper's own test. Cold, archived and cloud
cards have nothing to sleep and say so.

New request SleepAgent (protocol 52), only sent to a daemon that
speaks it.
@webdevcody

Copy link
Copy Markdown
Contributor

hmm I think maybe there is a bug with the idle reaper as well. it should sleep any session that hasn't had activity based on the idle timeout setting. I'll have to look into this, but thanks for the PR, I will merge after I research.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants