Skip to content

(sessions): send a running session to background and bring it back, so it survives a Switchboard restart #546

Open

Description

@devsuitup

Restarting Switchboard, for an update for instance, kills every Claude session running in its terminals. That includes long autonomous sessions in the middle of a task, with their subagents and session crons. Wanted: a "Send to background" action before the restart, and "Bring to front" afterwards.

What exists (study 11/10, CLI 2.1.296; docs and code read, nothing measured live)

  • The CLI has a native /background (alias /bg), per https://code.claude.com/docs/en/agent-view, "From inside a session":
    • It starts a new process that resumes from the saved conversation.
    • A turn in progress continues there.
    • Background shells, background subagents, dynamic workflows and /loop tasks carry over, Windows included since 2.1.195/196.
    • Monitors, and any subagent that owns a monitor, are stopped. The CLI asks "Background this session?" when such work is running.
    • Unsent text cancels it ("Backgrounding cancelled — you have unsent text").
  • Bringing a session back already works: guardResume answers {attach} for a live kind:'bg' descriptor, so a click in the sidebar opens a claude attach <jobId> tab. Restore and reload skip sessions that are live elsewhere.

Missing

  • The trigger: write /bg into the PTY, wait for the kind:'bg' descriptor and its jobId (with a timeout), then turn the tab into an attach tab or close it.
  • parkedJobId in ~/.claude/sessions/<pid>.json: the binary reads it and the CLI excludes such descriptors from its own liveness checks. Switchboard ignores it, so it probably duplicates the session in live-elsewhere and in the Agents view (not verified).
  • trigger-watcher.js targets PTYs by sessionId, so a /compact injected into a session that went to the background would fail with target process not running. It needs routing to the attach client or the worker's messaging socket.
  • Disable the action for remote, sandboxed and preLaunch sessions: the daemon starts the worker outside the sandbox (bg-agents invariant 8).

Proposed UX

  • "Send to background" in the session tool bar and in the sidebar row's context menu.
  • On quit or update, when sessions are busy: "Move N sessions to background and quit".
  • "Bring to front" is the existing attach path.

Measure live before building (isolated instance, throwaway session)

  1. After /bg, does the tab's claude exit or stay parked? Capture the descriptors before and after (same sessionId? parkedJobId? a new kind:'bg' with a jobId?) and the process tree.
  2. Quit Switchboard, then kill it: do the worker and the supervisor survive the ConPTY closing on Windows?
  3. After a restart: does the sidebar offer attach, and is the row free of an External + bg duplicate?
  4. What carries over: /loop, a non-/loop cron, a Monitor, a running subagent, a background shell.
  5. What a trigger-injected /compact does to a session in the background.

Activity

  1. devsuitup commented on Oct 11, 2026

    @devsuitup
    OwnerAuthor

    Live test, 11/10, CLI 2.1.296, Windows. An interactive session in a Switchboard tab ran /bg, then Switchboard was quit and restarted, and the session was reattached from the sidebar.

    What Result
    The conversation continued yes, with full context
    sessionId changed: 6577a487… → a9f4e948…. The new descriptor is kind:"bg", its jobId is the first 8 hex of the new sessionId, and its name gained a suffix (switchboard-dev (2))
    Session cron (CronCreate, not /loop) survived
    In-process background subagent (Agent with run_in_background) killed: "didn't finish before the previous session ended… in-process state was lost". The doc says background subagents carry over; here this one did not. It had partly edited a worktree, and the edits stayed on disk
    Session scratchpad (%TEMP%/claude/<project>/<sessionId>/scratchpad) the new session gets a new path, keyed by sessionId. The old files are still on disk, but the session no longer points at them
    Harness continuity the new session came up UNMANAGED and had to be registered again under the new id

    Consequences for the design:

  2. devsuitup commented on Oct 11, 2026

    @devsuitup
    OwnerAuthor

    Another gap observed live (11/10): for a session reattached after /bg, the sidebar's subagent indicators do not show running subagents. The data exists: ~/.claude/projects/<project>/<sessionId>/subagents/agent-*.jsonl keep being written as the two background subagents work. Probably the live subagent state is keyed on the Switchboard-owned PTY session, while an attach tab is keyed by its jobId. Not verified in the code.

  3. devsuitup commented on Oct 11, 2026

    @devsuitup
    OwnerAuthor

    Two more asks from JB (11/10, v0.0.94):

    1. Attach on a session that is already attached should go to its tab, not attach a second time. attachBgAgent (public/agents-view.js:386) calls openSession(session, { type: 'attach', … }), which should only show an already-open entry. Suspected cause, not verified: the attached tab is keyed under a different id than entry.sessionId. After /bg the session id changes, so the tab may sit under the old id. The roster already flags attachedHere (bg-agents-roster.js:211). Ask: when the job is attached here, Attach becomes "Go to tab" (or the same button focuses the tab). Test it with an attach entry whose tab key differs from the job's current session id.
    2. Detach from the sidebar without stopping the agent. stop-session already detaches an attach session instead of killing it (test "stop-session detaches an attach session instead of killing it"). But the sidebar shows it as Stop, the same control that kills a regular session. Ask: on an attached tab, label the control Detach, with a tooltip saying the agent keeps running in the background, and keep a separate, confirmed Stop for actually stopping the job.
  4. devsuitup commented on Oct 11, 2026

    @devsuitup
    OwnerAuthor

    JB: the reference model is the remote SSH/tmux sessions (docs/remote-hosts.md). There, leaving the tab detaches and the session keeps running on the host, while stopping it is a separate, explicit action. An attached background agent should behave the same way: same controls, same wording, same 'still running elsewhere' state on the sidebar row.

  5. devsuitup commented on Oct 11, 2026

    @devsuitup
    OwnerAuthor

    The product audit at v0.0.94 (docs/product/CONCEPT-MAP.md M2, #561) adds facts to the Detach part of this issue.

    One stop control, three meanings. The same square means:

    • kill (local);
    • remote kill;
    • detach (attached background job).

    Its label changes with the surface:

    Surface Label
    Row, grid card "Stop session" (public/sidebar.js:1766, public/grid-view.js:192)
    Terminal header "Stop process" (public/index.html:112)
    Terminal header, when attached "Detach (the session keeps running)" (public/app.js:1457)

    What a detach shows. It sets stopRequested (main.js:1789-1791). The grid card footer then reads Stopped and the exit banner reads "session stopped" while the job is still running.

    Other gaps:

    • Stop in the Agents view kills the job without a confirmation.
    • A remote attach (SSH/tmux) has no detach control at all.

    Proposed convergence: two distinct controls on every surface, never one icon with three meanings:

    • Detach: reversible, never asks, the session keeps running (the ssh/tmux model already noted above).
    • Stop: kills, and confirms when the process is live.
    • An already-attached session gets Go to tab instead of a second attach.
  6. devsuitup commented on Oct 13, 2026

    @devsuitup
    OwnerAuthor

    Spec written (13/10, against f0b3094), kept locally for now. Split into four slices:

    1. Send to background on one tab.
      • Half A: the action itself. It writes /bg, waits for the parked descriptor and the kind:"bg" descriptor with its jobId, then hands the tab over.
      • Half B: saved entries carry a bg mark, so restore reattaches them on every start.
    2. Move N sessions to background and quit or update, together with (sessions): quitting or updating kills running local sessions without warning #562.
    3. Separate Detach and Stop controls, and Go to tab ((state): one state vocabulary across sidebar, grid, header and Agents view #563, (ux): one confirmation rule for destructive vs reversible actions #565).
    4. The rest: /bg typed by hand (parkedJobId), triggers ((triggers): serialize and verify trigger delivery per tab (split from #579) #592), and subagent indicators.

    Found while writing it: today a backgrounded session is reattached at the first restart only. At the second restart the saved entry is kept but never reattached ("Not reopened: … is live in pid …"). Half B fixes this and needs no live measurement.

    Maintainer decision on Q2: when subagents are running in the tab, ask for confirmation, naming how many will be lost, with Send to background and Cancel.

    Still open:

    • Q1, what the tab becomes after the handoff, is with the product review.
    • Half A also waits on two live measurements: what the descriptor holds after /bg, and whether the worker survives when the parked tab is killed.
  7. devsuitup commented on Oct 13, 2026

    @devsuitup
    OwnerAuthor

    Q1 decided by the product review (13/10): convert the tab in place into a claude attach <jobId> tab under the new id, which matches the remote-session model (docs/remote-hosts.md:205-208; docs/background-agents.md:126-133).

    • The tab keeps its position, focus and name.
    • The notice reads: " now runs in the background. Closing this tab detaches it; it keeps running, and Switchboard reattaches it at the next start."
    • The header control keeps the existing attach label, "Detach (the session keeps running)". Detaching asks for no confirmation.
    • The sidebar row for the new id shows the bg badge at once, without waiting for the Agents view.
    • Stopping the job stays in the Agents view.

    Exceptions:

    • If killing the parked CLI kills the worker (measurement M2), fall back to leaving the parked window in place.
    • On slice 2's "move to background and quit" path, skip the conversion: close the parked CLI, write the bg mark and quit.
  8. devsuitup commented on Oct 13, 2026

    @devsuitup
    OwnerAuthor

    Live measurement M1/M2/M3, 13/10, CLI 2.1.296 on Windows 11. A real claude.exe ran in a node-pty ConPTY in a throwaway cwd; /bg, then 300 ms, then \r were written programmatically, the way Switchboard writes them. Raw logs: .work-files/spec-546-probe/live-m1m2/.

    • M3: yes. The programmatic /bg plus Enter runs /background (bg) through the completion menu.
    • M1: /bg does not park the tab.
      • At +65 ms the tab's descriptor goes waiting / dialog open.
      • At +252 ms that descriptor is deleted.
      • At +2011 ms the PTY exits by itself with code 0.
      • parkedJobId never appears, and the agents list is not shown.
      • The screen prints backgrounded · <jobId> and the attach, logs and stop hints, then the process exits.
    • Proof available (the spec's fallback branch):
      • About 0.2 s after the Enter, the old transcript gains {"type":"continued-in","continuedInSessionId":"<new>"}.
      • At +1.6 s a live descriptor appears with kind:"bg", jobId = the first 8 hex digits of the new sessionId, and the same cwd. It has no status for about 0.7 s, then idle.
      • The worker runs claude.exe --session-id <new> --fork-session --resume <old>.jsonl under --bg-pty-host … 120 40, so it inherits the tab size.
    • M2: the job survives the tab's process ending. claude agents --json --all shows it as idle/blocked, and the worker pid stays alive. Closing a parked CLI is moot, because nothing stays parked.

    Consequence for slice 1, half A: Switchboard sees an ordinary PTY exit after /bg. It converts the tab using continued-in plus the live bg descriptor. It must tolerate the bg descriptor's first ~0.7 s with no status. There is no parked process to kill.

  9. devsuitup commented on Oct 15, 2026

    @devsuitup
    OwnerAuthor

    Slice 2 (with #562) — decided by Jean-Baptiste in person, 2026-10-15, after an independent refutation of the slice-2 spec:

    1. Mixed population at quit / Restart / window close: one button "Move K sessions to background, stop M (listed by name), and quit/update/close". Plain terminals and panel shells are always on the stop side and never disable Background. Follows (ux): one confirmation rule for destructive vs reversible actions #565: a kill confirms by naming what will be lost.
    2. Busy sessions: measure a /bg sent during a turn ((restore): a backgrounded session is restored from its pre-fork transcript #518 reports the CLI deferring it until the current tool finishes) on its own issue, and lift slice 1's busy refusal if it is safe. Until then, the dialog waits for busy sessions to become idle, within a bound the user can cancel.
    3. Delivery: two PRs. 2a — the quit/Restart/close dialog with counts, Stop and Cancel, external-exit bypass, attempt-scoped approvals; closes (sessions): quitting or updating kills running local sessions without warning #562; lands after (index): move every session index write to a writer worker (#616) #646 and (editor): open, read and edit files outside git and beyond the Touched list (#658) #659. 2b — Background-and-quit (handoffs, re-key without attach, checked save, abort, recovery).

    Spec facts the revision must carry (verified 15/10 against 76edc5c and #646 head 0ff13e7):

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions