Session resume
A Tesseron session lives somewhere — the gateway's memory for gateway-minted sessions, the host's memory for host-minted sessions (e.g. behind @tesseron/vite). When the underlying WebSocket drops (tab refresh, window close, network blip, HMR reload), the session would normally go away and a reconnecting app would have to go through the full tesseron/hello + claim-code dance again — even if the user had already paired it.
The tesseron/resume method lets the app rejoin an existing session it paired earlier, skipping the re-claim. Storage of the resume credentials is deliberately the implementer's responsibility: different apps have different opinions about where session credentials can live (localStorage, a cookie, an Electron store, the OS keychain), so the SDK exposes the primitive and leaves the choice to you.
Both mint flows (gateway-minted and host-minted) honour resume:
- Gateway-minted sessions (Node-side hosts via
@tesseron/server): the gateway holds a zombie of the closed session forresumeTtlMsand reattaches the new socket on a matching{ sessionId, resumeToken }. - Host-minted sessions (browser tabs via
@tesseron/vite): the host holds the Session in memory forsessionIdleTtlMsand reattaches the new browser WebSocket on a matching{ sessionId, resumeToken }. The gateway-side bridge stays open across the detach — the agent sees no disconnect.
- On a fresh
tesseron/hello, the gateway returns aresumeTokenin the welcome. Stash it alongside thesessionIdwherever fits your app. - When the transport drops, the gateway keeps the session's metadata as a "zombie" for
resumeTtlMs(default 4 hours, configurable via env varTESSERON_RESUME_TTL_MSor per-gatewaynew TesseronGateway({ resumeTtlMs })). - On reconnect, the app sends
tesseron/resumewith{ sessionId, resumeToken }. If the token matches (constant-time compare) and the zombie is still within its TTL, the gateway reattaches the fresh socket to the existing session and rotates the token. - The caller persists the new
resumeTokenfrom the resume response.
Resume tokens are one-shot: every successful resume rotates the token and the previous value stops working. This means the freshest welcome is always the one to persist.
The tesseron/resume request
Section titled “The tesseron/resume request”{ "jsonrpc": "2.0", "id": 1, "method": "tesseron/resume", "params": { "protocolVersion": "1.1.0", "sessionId": "s_a1b2c3de1234567", "resumeToken": "Xk9f3nN9kOeGqR7mWpLc2v", "app": { "id": "shop", "name": "Acme Shop", "origin": "http://localhost:3000" }, "actions": [ /* same shape as tesseron/hello */ ], "resources": [ /* same shape as tesseron/hello */ ], "capabilities": { "streaming": true, "subscriptions": true, "sampling": true, "elicitation": true } }}ResumeParams carries the same app / actions / resources / capabilities as HelloParams because a fresh app build may have added, removed, or changed them since the previous connect. The gateway replaces the stored manifest with what resume brings in.
The resume response
Section titled “The resume response”{ "jsonrpc": "2.0", "id": 1, "result": { "sessionId": "s_a1b2c3de1234567", "protocolVersion": "1.1.0", "capabilities": { "streaming": true, "subscriptions": true, "sampling": true, "elicitation": true }, "agent": { "id": "claude-ai", "name": "Claude Desktop" }, "resumeToken": "NEW_ROTATED_TOKEN_VALUE" }}sessionIdmatches the one in the request (same session, reattached to the fresh socket).resumeTokenis rotated. Overwrite whatever you stashed with this new value.claimCodeis omitted - the session is already claimed, no need for a re-pair.
Failure modes
Section titled “Failure modes”All resume failures surface as a TesseronError with code TesseronErrorCode.ResumeFailed (-32011). Callers typically catch the error and fall back to a plain tesseron/hello.
| Condition | Message pattern |
|---|---|
Unknown sessionId | No resumable session "<id>" |
| Cross-app resume | Session "<id>" is owned by app "<other>" |
| Unclaimed zombie | <id> was never claimed |
| TTL elapsed | Falls under "no resumable session" - the zombie was already evicted |
Wrong resumeToken | Invalid resumeToken for session "<id>" |
Malformed params (missing app, non-string sessionId / resumeToken, missing actions / resources / capabilities) | Invalid tesseron/resume request: expected { protocolVersion, sessionId, resumeToken, app, actions, resources, capabilities } |
| Protocol major-version mismatch | Same rules as tesseron/hello - throws ProtocolMismatch |
Token comparison uses crypto.timingSafeEqual with a length pre-check, so a wildly-wrong token doesn't leak timing information about the correct length.
Gateway configuration
Section titled “Gateway configuration”Two knobs on new TesseronGateway({ ... }) shape how aggressive resume is:
const gateway = new TesseronGateway({ resumeTtlMs: 300_000, // 5 minutes (default: 14_400_000 / 4 hours) maxZombies: 500, // cap on zombies held simultaneously (default: 100)});resumeTtlMs— how long a closed session is retained as a resumable zombie. Default 4 hours; long enough to span a normal working session (casual refreshes, dev-server restarts, lunch breaks, brief laptop sleep) without forcing the user back through the claim-code dance. Set to0to disable resume entirely: closed sessions drop immediately and any reconnect must start fresh. The@tesseron/mcpCLI also reads theTESSERON_RESUME_TTL_MSenv var (non-negative integer milliseconds) so operators can tune it without forking the gateway.maxZombies— ceiling on the in-memory zombie map. When inserting a new zombie would exceed it, the oldest (longest-retained) zombie is evicted to make room. Keeps a connect/disconnect flood from piling up zombies faster than their TTLs expire. Set to0to disable resume entirely (same effect asresumeTtlMs: 0).
Idiomatic SDK usage
Section titled “Idiomatic SDK usage”@tesseron/web auto-persists by default. From 2.9.0, tesseron.connect() loads stored credentials, sends tesseron/resume, saves the rotated token, and transparently falls back to a fresh tesseron/hello if the resume fails — no glue code required:
import { tesseron } from '@tesseron/web';
tesseron.app({ id: 'shop', name: 'Acme Shop' });tesseron.action('searchProducts').handler(/* ... */);
const welcome = await tesseron.connect();// Refresh the page → next connect resumes the same session,// agent stays paired, no new claim code.Refresh costs nothing inside the TTL window. The default backend is localStorage under the key tesseron:resume. To use a different key, pass a string. To opt out of persistence (incognito-style flows), pass resume: false. To run a custom backend (OS keychain, Electron store, IPC channel), implement ResumeStorage:
import { tesseron, type ResumeStorage } from '@tesseron/web';
const keychainBackend: ResumeStorage = { load: () => ipc.invoke('tesseron:load'), save: (creds) => ipc.invoke('tesseron:save', creds), clear: () => ipc.invoke('tesseron:clear'),};
await tesseron.connect(undefined, { resume: keychainBackend });If you've already loaded credentials yourself and just want to forward them, pass a ResumeCredentials literal — the SDK uses it as-is and does not auto-persist (that's your job):
await tesseron.connect(undefined, { resume: { sessionId, resumeToken }, // explicit; SDK won't write to localStorage});If you're using @tesseron/react, the useTesseronConnection hook bakes in the same flow and exposes resumeStatus ('none' | 'resumed' | 'failed') for UIs that want to show "your previous session expired" instead of silently rendering a new claim code.
Falling back when resume fails
Section titled “Falling back when resume fails”try { await tesseron.connect(url, saved ? { resume: JSON.parse(saved) } : undefined);} catch (err) { if (err instanceof TesseronError && err.code === TesseronErrorCode.ResumeFailed) { localStorage.removeItem('tesseron:shop'); await tesseron.connect(url); // fresh hello } else { throw err; }}What resume does not do
Section titled “What resume does not do”- It does not replay in-flight actions. An action the agent invoked just before the socket dropped is cancelled on the gateway; the agent sees an error (see lifecycle) and can retry at its own layer.
- It does not resurrect resource subscriptions. The SDK re-subscribes on reconnect as it does after any handshake.
- It does not persist across a gateway restart. Zombies live in gateway process memory; stopping the gateway evicts them. A fresh
tesseron/hellois required after any gateway restart. - It does not work for sessions that were never claimed. The gateway surfaces
ResumeFailedwithnever claimedso the SDK can fall back totesseron/hellowithout ambiguity.
See also
Section titled “See also”- Handshake & claiming - the
tesseron/helloflow resume complements. - Lifecycle & failure modes - how the gateway behaves during drops, retries, and gateway restarts.
- Errors & capabilities - the full
TesseronErrorCodetable includingResumeFailed.