12. Session continuity — across pages and tabs
Status: implemented in
webagent/src/core/WebAgent.ts+session.ts.
What "session" means here
A AgentSession is the agent's running state for one task:
interface AgentSession {
id: string;
task: string; // the user's instruction
steps: AgentStep[]; // tool calls so far
status: AgentStatus;
currentPage: string; // URL pathname at last step
startedAt: number;
updatedAt: number;
summary?: string;
}
Webagent persists this each step. The three places it can live, and what each unlocks:
| Storage | Same tab nav | Same-origin new tab | Cross-origin | New device |
|---|---|---|---|---|
sessionStorage |
✅ | ❌ | ❌ | ❌ |
localStorage + BroadcastChannel |
✅ | ✅ | ❌ | ❌ |
| Host backend | ✅ | ✅ | ✅ | ✅ |
Same-tab navigation (default — sessionStorage)
Out of the box, every webagent instance saves to sessionStorage[webagent.session] on every step. When the agent calls navigate('/billing'):
- The URL changes (host's
onNavigatehandler). - React re-renders, host re-mounts dddk on the new page.
- New dddk instance constructs a new WebAgent.
- The WebAgent loop notices the URL change → updates
session.currentPageand stampspreviousUrlso the next prompt includes "You arrived from/crm".
sessionStorage is per-origin per-tab, so this works for any same-origin navigation including subdomains? No — subdomains have separate sessionStorage. See "Cross-subdomain" below.
To resume on page load (e.g. user refreshed):
const agent = new WebAgent({ llm });
await agent.resume(); // hydrates from sessionStorage if a session exists
dddk does this automatically on mount().
Cross-tab (same origin) — crossTabSync: true
new WebAgent({ llm, crossTabSync: true });
// or via dddk:
new DotDotDuck({ llm, webAgent: { crossTabSync: true } });
What it does:
localStoragemirror — every session save also writes tolocalStorage[webagent.session.crosstab]. A new tab opening on the same origin will read it during construction.BroadcastChannel— broadcasts the session object to other tabs in real time. Other tabs adopt the broadcast if they are idle (not currently running their own task) and the broadcast is newer than their local session.
So:
- User starts a task in Tab A → opens Tab B on the same origin → Tab B picks up where Tab A left off.
- Both tabs idle → either can resume the task.
- Tab A is mid-run → Tab B sees the update but waits politely (only adopts when idle).
What it doesn't do:
- It does not allow two tabs to drive the SAME running agent. Last-writer-wins for the conversation history.
- It does not cross origins (browser sandbox).
Cross-subdomain (e.g. app.acme.com ↔ docs.acme.com)
The browser treats subdomains as separate origins for localStorage/sessionStorage. Two patterns:
Pattern A — cookie + URL hash handoff
Host code on the source page:
// On the link that goes to docs.acme.com:
const sessionId = dddk.getAgent()?.getSession()?.id;
window.location.href = `https://docs.acme.com/page?dddkSession=${sessionId}`;
On the destination subdomain, host reads the URL hash and asks the API for the session:
const id = new URL(location.href).searchParams.get('dddkSession');
if (id) {
const session = await fetch(`/api/dddk/session/${id}`).then(r => r.json());
dddk.getAgent()?.adoptSession(session); // future API
}
This requires host-side storage (your backend) — there's no client-side cross-origin store.
Pattern B — third-party iframe
Less recommended (UX is worse). Embed the secondary subdomain as iframe and proxy webagent through postMessage.
Cross-origin (different parent domains)
Not supported client-side, period. If you have two separate brands (acme.com and umbrella-corp.com) sharing a session, you need a host backend session store. Use the agent's clearSession() / getSession() to ship JSON over your wire.
What the agent sees about navigation
Each turn, the prompt includes:
- You arrived from: /crm
…if currentPage changed since the previous step. The agent can use this to make sense of links it followed.
When to flip crossTabSync on
Default: off — it costs a localStorage write + a BroadcastChannel message per step. For most demos that's fine, but on a hot session loop it adds a few ms.
Turn it on when:
- Users actually open new tabs mid-task (common for shop / CRM workflows).
- You have a multi-product suite where users jump between products in new tabs.
Leave it off when:
- Single-product app where users stay in one tab.
- You're using a host backend session store anyway (your store wins).
What the example demo does
dddk/example_website/src/App.tsx sets crossTabSync: true so you can:
- Start a task on
/crm. Ctrl+Topen new tab, pastehttp://localhost:5174/crm.- New tab adopts the conversation history.
Try it on the Try-It page — step 7.