diff --git a/README.md b/README.md index 27e8af0..9f71ac6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/content.json b/docs/content.json index 6a3660d..02a2411 100644 --- a/docs/content.json +++ b/docs/content.json @@ -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."] diff --git a/shared/session-lifetime.ts b/shared/session-lifetime.ts new file mode 100644 index 0000000..97498a3 --- /dev/null +++ b/shared/session-lifetime.ts @@ -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); +} diff --git a/tests/session-lifetime.test.ts b/tests/session-lifetime.test.ts new file mode 100644 index 0000000..b69fe2b --- /dev/null +++ b/tests/session-lifetime.test.ts @@ -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); + }); +}); diff --git a/worker/index.ts b/worker/index.ts index 7f28fb2..56a5b20 100644 --- a/worker/index.ts +++ b/worker/index.ts @@ -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, @@ -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; @@ -1220,7 +1222,7 @@ export class TerminalSession extends DurableObject { 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(); @@ -1257,7 +1259,7 @@ export class TerminalSession extends DurableObject { 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(); }