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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ The single stderr line normally contains `share_url`, `e2ee_password`, `session_
- A 512 KiB local ring buffer restores newly connected viewers. In E2EE mode, snapshots are encrypted before leaving the CLI. Terminal output is not retained by Cloudflare after the task closes.
- A relay ping/pong measures browser-to-machine round-trip latency; the UI reports `Offline` when the local CLI cannot answer.

An active ordinary process renews its lease indefinitely. A disconnected ordinary process has a 15-minute reconnect grace period. Its completed process closes sockets and deletes Durable Object state immediately. Persistent Docker sessions are the explicit exception: the relay keeps their non-content identity for up to 30 offline days so the same state volume can reconnect the same URL. Opening an expired ordinary link shows that the session no longer exists.
An active ordinary process renews its lease indefinitely. If its machine sleeps or loses the network, the same link remains recoverable for 12 hours while the CLI reconnects automatically. Its completed process closes sockets and deletes Durable Object state immediately. Persistent Docker sessions are the explicit exception: the relay keeps their non-content identity for up to 30 offline days so the same state volume can reconnect the same URL. Opening an expired ordinary link shows that the session no longer exists.

## Terminal reliability guarantees

Expand Down
2 changes: 1 addition & 1 deletion docs/content.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"title": "The link can disappear. Your process should not.",
"intro": "The PTY and command live on your machine. Relay and browser failures are treated as recoverable display failures, never as permission to terminate local work.",
"cards": [
["Automatic reconnect", "The CLI and browser reconnect with bounded backoff. A temporary network failure does not stop the process, and disconnected ordinary sessions retain a 15-minute relay grace window."],
["Automatic reconnect", "The CLI and browser reconnect with bounded backoff. A temporary network failure does not stop the process, and the same ordinary share link remains recoverable for 12 hours after its host disconnects."],
["Screen recovery", "A bounded local ring buffer restores new or returning viewers. If live output outruns either network or rendering, shell.online drops stale display work and sends one authoritative screen snapshot."],
["Backpressure by design", "PTY reads never wait indefinitely for Cloudflare. WebSocket writes time out, queues are bounded, frames have size and traffic limits, and high-output processes keep running locally."],
["Explicit lifecycle", "The URL prints only after a short startup usability handshake. If the task has already exited, shell reports its status without printing a dead link or attach/kill instructions. shell list distinguishes local process liveness from relay status; shell attach rejoins locally and shell kill stops the process. Ordinary shares and server state disappear when the task exits."]
Expand Down
6 changes: 6 additions & 0 deletions shared/session-lifetime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const SESSION_TTL_MS = 12 * 60 * 60 * 1_000;
export const PERSISTENT_TTL_MS = 30 * 24 * 60 * 60 * 1_000;

export function disconnectedSessionExpiry(now: number, persistent: boolean): number {
return now + (persistent ? PERSISTENT_TTL_MS : SESSION_TTL_MS);
}
18 changes: 18 additions & 0 deletions tests/session-lifetime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import {
disconnectedSessionExpiry,
PERSISTENT_TTL_MS,
SESSION_TTL_MS,
} from "../shared/session-lifetime";

describe("disconnected session lifetime", () => {
it("keeps an ordinary share recoverable for twelve hours", () => {
const now = Date.UTC(2026, 8, 4, 12, 0, 0);
expect(disconnectedSessionExpiry(now, false)).toBe(now + SESSION_TTL_MS);
});

it("retains the persistent thirty-day recovery window", () => {
const now = Date.UTC(2026, 8, 4, 12, 0, 0);
expect(disconnectedSessionExpiry(now, true)).toBe(now + PERSISTENT_TTL_MS);
});
});
12 changes: 7 additions & 5 deletions worker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import { RELEASE_VERSION } from "../shared/release";
import { viewerFrameAction } from "../shared/session-access";
import { terminalGridForDevices } from "../shared/terminal-grid";
import { persistentSessionID } from "../shared/persistent-session";
import {
disconnectedSessionExpiry,
PERSISTENT_TTL_MS,
SESSION_TTL_MS,
} from "../shared/session-lifetime";
import {
GITHUB_REPOSITORY_API_URL,
GITHUB_REPOSITORY_URL,
Expand All @@ -38,9 +43,6 @@ import {

export { StatsStore };

const SESSION_TTL_MS = 12 * 60 * 60 * 1000;
const DISCONNECTED_GRACE_MS = 15 * 60 * 1000;
const PERSISTENT_TTL_MS = 30 * 24 * 60 * 60 * 1000;
const MAX_VIEWERS = 16;
const MAX_LIVE_FRAME_BYTES = 64 * 1024;
const MAX_INPUT_FRAME_BYTES = 16 * 1024 + 1;
Expand Down Expand Up @@ -1220,7 +1222,7 @@ export class TerminalSession extends DurableObject<Env> {
return;
}
this.meta.status = "disconnected";
this.meta.expiresAt = Date.now() + (this.meta.persistent ? PERSISTENT_TTL_MS : DISCONNECTED_GRACE_MS);
this.meta.expiresAt = disconnectedSessionExpiry(Date.now(), this.meta.persistent);
await this.persistMeta();
await this.refreshLivePresence(true, socket);
await this.scheduleNextAlarm();
Expand Down Expand Up @@ -1257,7 +1259,7 @@ export class TerminalSession extends DurableObject<Env> {

if (this.meta.status === "connected") {
this.meta.status = "disconnected";
this.meta.expiresAt = Date.now() + DISCONNECTED_GRACE_MS;
this.meta.expiresAt = disconnectedSessionExpiry(Date.now(), false);
await this.persistMeta();
this.broadcastStatus();
}
Expand Down
Loading