Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ not lost, but nothing runs until it wakes.

The full version is at [flue.sh/docs/setup](https://flue.sh/docs/setup).

## Sessions outlive flue

Every session runs in its own small holder process, not inside the daemon.
Updating flue, restarting it, even the daemon crashing outright: the shells
and agents keep running, and the next daemon picks them back up with their
scrollback where you left it. A machine reboot is the one thing that ends a
session, and even then flue brings it back with its history, a fresh shell,
and the command that resumes the agent conversation it was in.

## The CLI

```
Expand Down
10 changes: 9 additions & 1 deletion cmd/flue/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const usageText = `flue — your terminal, as a browser tab

flue enable install the login service, start the daemon, open the UI
flue disable remove the login service
flue restart restart the daemon; live sessions ride across on snapshots
flue restart restart the daemon; sessions keep running through it
flue status daemon, login service, and session diagnostics
flue relay setup deploy a relay to your own Cloudflare account
flue relay join URL --secret S --fleet K point this machine at an existing relay
Expand Down Expand Up @@ -1611,6 +1611,14 @@ func runRestart(w io.Writer, wait time.Duration) error {
return err
}
fmt.Fprintf(w, " ✓ daemon running on 127.0.0.1:%d\n", port)
// True by construction, not aspiration: sessions live in their own
// holder processes, and the daemon that just restarted reattached to
// them on its way up. The one exception is a daemon running with
// FLUE_NO_HOLDER, whose sessions still ride snapshots; that escape
// hatch's users chose the old behaviour and know what it costs.
if os.Getenv("FLUE_NO_HOLDER") == "" {
fmt.Fprintf(w, " ✓ sessions kept running; tabs reconnect on their own\n")
}
return nil
}

Expand Down
6 changes: 6 additions & 0 deletions cmd/flue/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,12 @@ func restartForUpdate(w io.Writer, latest string) error {
return err
}
fmt.Fprintf(w, " ✓ daemon restarted, running flue %s on 127.0.0.1:%d\n", got, port)
// Same honesty rule as runRestart: holder-backed sessions rode
// across the bounce; only the FLUE_NO_HOLDER escape hatch still
// pays for an update with its sessions.
if os.Getenv("FLUE_NO_HOLDER") == "" {
fmt.Fprintf(w, " ✓ sessions kept running; tabs reconnect on their own\n")
}
return nil
}
}
Expand Down
17 changes: 15 additions & 2 deletions site/src/routes/docs.faq.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,21 @@ const QUESTIONS: { q: string; verdict: string; body?: ReactNode }[] = [
verdict: 'Nothing. Closing detaches the session. It does not kill it.',
body: (
<P>
The daemon owns the shell and its scrollback, so the build keeps running. Reattach from any
device and it replays what you missed.
The session owns its shell and its scrollback, so the build keeps running. Reattach from
any device and it replays what you missed.
</P>
),
},
{
q: 'What happens to my sessions when flue updates or restarts?',
verdict: 'Nothing. Sessions outlive the daemon.',
body: (
<P>
Every session runs in its own small holder process, separate from the daemon. Update flue,
restart it, even have it crash: the shells and agents keep running, and the next daemon
picks them back up with their scrollback intact. A machine reboot is the one thing that
ends a session, and even then flue brings it back with its history, a fresh shell, and the
command that resumes the agent conversation it was in.
</P>
),
},
Expand Down
13 changes: 9 additions & 4 deletions site/src/routes/docs.how-it-works.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ const COMMANDS: { cmd: string; what: string }[] = [
function HowItWorks() {
return (
<DocPage slug="how-it-works" title={DOC.title} blurb={DOC.blurb}>
<Section title="One daemon owns the shells">
<Section title="Sessions own their shells">
<Lead>
A small Go daemon holds the terminals and their scrollback. Everything that draws them is
a client, including the tab you are reading this in.
Every session is a small process of its own that holds the shell and its scrollback. A Go
daemon coordinates them, and everything that draws them is a client, including the tab
you are reading this in.
</Lead>
<P>
The whole product rests on that one move. Normally a shell belongs to the window that
Expand All @@ -38,9 +39,13 @@ function HowItWorks() {
</P>
<P>
Closing the tab does not kill the session. It only detaches it. The build keeps running,
the agent keeps working, the SSH session stays up. Reattach and the daemon replays the
the agent keeps working, the SSH session stays up. Reattach and the session replays the
scrollback you missed.
</P>
<P>
It outlives the daemon too. Updating flue, restarting it, even a crash: sessions keep
running through all of it, and the next daemon picks them back up where they were.
</P>
</Section>

<Section title="Every device is another view">
Expand Down
3 changes: 2 additions & 1 deletion web/src/components/update-notice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,8 @@ function UpgradeDialog({
<DialogTitle>{latest ? `Update to flue ${latest}` : 'Update flue'}</DialogTitle>
<DialogDescription className="text-sm/6 text-pretty text-zinc-600 sm:text-xs/5 dark:text-zinc-400">
Replacing the binary is something only this machine can do, so it happens in a terminal
rather than here. Whichever way flue was installed, run its line and restart the daemon.{' '}
rather than here. Whichever way flue was installed, run its line and restart the
daemon. Your sessions keep running through the restart.{' '}
{url !== undefined && (
<>
<TextLink href={url} target="_blank">
Expand Down