Skip to content

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.

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.

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.

const session = await pedelec.resumeSession(savedSessionId);

resumeSession():

  1. requires a non-empty session ID;
  2. triggers origin approval if the current origin is not approved;
  3. subscribes the current SDK route to the existing Core thread;
  4. 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;
  5. returns a synchronized PedelecSession handle.

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.

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.

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.

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.

await session.end();

A successful end():

  • asks Core to end the session;
  • marks the SDK handle ended;
  • emits onEnded() once for this transition into ended;
  • unregisters the session from the client; and
  • causes future sendText()/prepare() calls to reject with SESSION_ENDED until same-handle resume() 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 on end(), 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.

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.

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.

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.