Session Lifecycle and Resume
Session lifetime is a product decision. Pedelec defaults to a page-scoped lifecycle, but applications can keep a Core session alive for later resume.
Page-scoped lifecycle
Section titled “Page-scoped lifecycle”const session = await pedelec.createSession({ provider: "codex", autoEndOnDisconnect: true,});true is the default. The extension tracks which SDK routes are attached to the session. When the final route disconnects, it asks Core to end the thread.
Common disconnect causes:
- tab close;
- page reload;
- navigation that destroys the JavaScript page;
- extension reload/update;
- browser or extension process interruption.
Use page-scoped sessions when the session should not outlive the visible application context.
Persistent lifecycle
Section titled “Persistent lifecycle”const session = await pedelec.createSession({ provider: "codex", autoEndOnDisconnect: false,});
const record = { sessionId: session.sessionId, provider: session.provider, effortLevel: session.effortLevel,};localStorage.setItem("pedelec-session", JSON.stringify(record));With false, loss of the SDK route does not automatically end the Core session. The application can later resume using the saved ID.
The browser-side PedelecSession handle remains bound to the SDK route that created it. If that runtime port disconnects, the handle becomes unusable even when the parent Pedelec client reconnects. Call resumeSession(sessionId) explicitly to subscribe a new handle to the surviving Core session; the SDK does not silently migrate old session operations. A handle that is explicitly ended is different: if its transport is still attached, the same object can call session.resume() to reactivate the ended Core thread.
Use this only when you have a deliberate retention and cleanup policy. A session that is never resumed or ended can remain in Desktop state longer than the UI implies.
Resume a session
Section titled “Resume a session”const session = await pedelec.resumeSession(savedSessionId);resumeSession():
- requires a non-empty session ID;
- triggers origin approval if the current origin is not approved;
- subscribes the current SDK route to the existing Core thread;
- reconciles the returned authoritative lifecycle snapshot, including status, active/last operation identity, and a pending tool request when present. An ended thread remains ended; this method never implicitly calls Core
resume_thread; - returns a synchronized
PedelecSessionhandle.
When Core already knows normalized token usage, the same resume snapshot hydrates session.usage.totalTokens. A missing usage field means that no supported total is known yet; it does not reset usage on an existing handle.
An empty ID rejects with INVALID_INPUT.
If Core no longer knows the ID, it returns THREAD_NOT_FOUND; remove stale persistence or create another session. If the session belongs to another browser origin, access rejects with THREAD_ACCESS_DENIED. The SDK does not silently create a replacement session.
Reactivate the same ended handle
Section titled “Reactivate the same ended handle”const session = await pedelec.createSession({ provider: "codex", autoEndOnDisconnect: false,});
await session.sendText("First task");await session.end();await session.resume();
console.log(session.getStatus()); // "idle"await session.sendText("Continue");PedelecSession.resume() is an explicit Ended → Idle Core lifecycle transition for the same non-detached browser handle. It preserves the thread ID, handlers, usage, provider/effort metadata, prepared state, and provider session identity. It does not start or contact the provider runtime; the next sendText() or other normal operation resumes provider work lazily.
Reactivation succeeds only while Core still knows the thread and its recorded workspace_path still exists and is a directory. Managed and explicit application workspaces are both supported. A missing or unusable workspace rejects with WORKSPACE_OPEN_FAILED; workspace contents alone cannot reconstruct a thread after a Desktop/Core restart. A missing Core thread rejects with THREAD_NOT_FOUND.
resume() is idempotent for an active handle, and concurrent calls on one ended handle share one request. A transport-detached handle rejects; use pedelec.resumeSession(sessionId) to attach a new handle to a surviving Core thread instead. The detached-handle rule also applies when the Core thread is ended.
Re-register every browser handler
Section titled “Re-register every browser handler”Handlers are JavaScript functions and cannot survive reload. For Pedelec.resumeSession(), resume first, then attach them again:
const session = await pedelec.resumeSession(record.sessionId);
const offChat = session.onChat(handleCompletedChat);const offChatDelta = session.onChatDelta(handleChatDelta);const offStatus = session.onStatus(handleStatus);const offError = session.onError(handleError);const offEnded = session.onEnded(handleEnded);const offTool = session.onTool("update_counter", handleUpdateCounter);Inline handlers from the original createSession({ skills }) call are not automatically reconstructed by a new page. The Core session still has its tool manifest, but the newly resumed browser must register handlers for any tools the agent may call. Same-handle session.resume() keeps its existing handlers, including inline handlers.
A generic fallback is useful during migration, but it should not replace deliberate per-tool validation.
Provider and effort metadata after reattachment
Section titled “Provider and effort metadata after reattachment”The resume bridge returns the sessionId plus an internal lifecycle snapshot, not the full Core session record. Therefore provider metadata may still be unavailable on a newly constructed resumed handle, while its lifecycle status is authoritative:
session.provider === "";session.effortLevel === undefined;If the UI needs these fields after reload, save them with the session ID or load them from application-owned persistence. Do not assume resumeSession() rehydrates all metadata, but do rely on getStatus() for the Core lifecycle state after it resolves. Same-handle session.resume() preserves the metadata already known by that object.
session.sessionCreatedAt is the time the browser-side handle was constructed, not the original Core session creation time.
Avoid resuming twice accidentally
Section titled “Avoid resuming twice accidentally”SSR hydration, React Strict Mode development behavior, or duplicated effects can call resumeSession() more than once. Guard reattachment in application state:
let resumePromise: Promise<PedelecSession> | null = null;
function resumeOnce(id: string) { resumePromise ??= pedelec.resumeSession(id); return resumePromise;}Clear or replace that guard deliberately when switching IDs.
End a session
Section titled “End a session”await session.end();A successful end():
- asks Core to end the session;
- marks the SDK handle
ended; - emits
onEnded()once for this transition intoended; - unregisters the session from the client; and
- causes future
sendText()/prepare()calls to reject withSESSION_ENDEDuntil same-handleresume()succeeds.
end() does not immediately delete the session workspace. The lifecycle depends on how the session was created:
- Without
workspace.path, the session uses temporary Desktop-managed storage. The Desktop App removes managed workspaces on normal app exit and attempts to clean up leftovers at the next launch after an abnormal termination. Closing the main window only hides the app and does not run this cleanup. - With an explicit absolute
workspace.path, the session uses an application-managed workspace. Pedelec never deletes it onend(), normal app exit, or stale-workspace cleanup, and multiple active sessions may share it.
The ended handle cannot access assets through uploadAsset(), listAssets(), or readAsset(), even before cleanup runs. Shared workspaces do not receive filesystem conflict coordination from Pedelec, and an explicit path must not overlap the managed workspace root. Locked files can leave managed cleanup for a later launch.
Calling end() again on the same already-ended handle resolves without another request.
session.resume() is the separate explicit reactivation operation; it requires the same handle’s transport to remain attached and does not reconstruct a thread from workspace contents.
If the end request fails, end() emits an SDK error and rejects. The session is not locally marked ended by that failed call, because Core state is uncertain.
Cleanup pattern
Section titled “Cleanup pattern”async function disposeSession() { const current = session; session = null;
try { if (current && current.getStatus() !== "ended") { await current.end(); } } finally { for (const dispose of handlerDisposers) dispose(); handlerDisposers = []; localStorage.removeItem("pedelec-session"); }}When using autoEndOnDisconnect: false, remove persisted IDs only after your product has decided the session is no longer recoverable or desired.
Multiple routes and tabs
Section titled “Multiple routes and tabs”The extension can route one session to more than one SDK connection. With automatic end enabled, the session ends only after the final route disappears. This enables shared observation but also creates coordination concerns:
- only one active turn is allowed by the session;
- two tabs can race and receive
SESSION_BUSY; - tool handlers may exist in more than one page context;
- application state can diverge across tabs.
Unless cross-tab collaboration is intentional, store an ownership token or prevent multiple active controllers.
If the Native Host or an individual thread subscription disconnects, the Extension restores the subscription and reconciles a new snapshot. It does not automatically resend an ambiguous sendText(), prepare(), or tool-result mutation. Keep operation IDs and promise errors in diagnostics, and let the recovered authoritative state determine the next user action.
Choosing a lifecycle
Section titled “Choosing a lifecycle”| Requirement | Suggested setting |
|---|---|
| Temporary assistant tied to one page | autoEndOnDisconnect: true |
| Resume after a full reload | false + persist sessionId |
| Multi-page workflow | false + explicit ownership/cleanup |
| Sensitive transient context | Prefer true and explicit end() |
| Long-running task | false, plus clear status and recovery UX |
Next, learn how the session can call frontend tools.