Context
Context is a free-form Record<string, unknown> that the client keeps in sync with the server. The autopilot reads it every turn — use it for everything that describes "where the user is right now": route, selection, app mode, theme, IDs.
Read
const ctx = client.getContext(); // returns a deep copyWrite — three flavors
client.setContext({ a: 1, b: 2 }); // replaces the entire context
client.updateContext({ b: 3, c: 4 }); // shallow merge → { a:1, b:3, c:4 }
client.mergeContext({ user: { name: 'Mo' } }); // deep mergeWhich one to use:
setContextwhen you compute the full context from scratch on every render.updateContextfor the common case — "this one key changed".mergeContextwhen you want to update a nested object without overwriting siblings.
Clear
client.clearContext();Sync model
All three writers feed the same pipeline:
your write ──► diff against last-synced snapshot ──► debounce 100 ms ──► send JSON PatchTwo consequences:
- Bursty writers (a
watchthat fires on every keystroke, say) collapse into a single network message. - The server only sees the net change, not your intermediate states.
Size and depth limits
Every context sync is pre-flighted against the relay's limits before anything goes on the wire:
- 64 KB cap. If the projected stored state (functions catalog + context) would exceed
MAX_STATE_SIZE_BYTES(65536 bytes), the sync is aborted and astateErrorevent fires — e.g.context sync rejected: projected state size X bytes exceeds relay cap of 65536 bytes. The pending change is not discarded; shrink the context and it retries. - 10-level depth. The relay silently drops JSON-Patch ops whose path is deeper than 10 segments. The client catches this upfront and fires a
stateErrornaming the offending path(s), so deep nesting fails loudly instead of diverging unnoticed.
client.on('stateError', ({ error }) => {
console.error('state rejected:', error.message); // shrink / flatten, then retry
});Server snapshot reconciliation
When a server context snapshot arrives (the first connect, a relay echo of your own write, or a state_change from another client), the client reconciles it with any not-yet-synced local edits:
- Unsent local edits are replayed on top of the server view — a write made between a flush and its echo is never lost.
- If a local edit cannot replay onto the server view (the server retyped or removed a parent it touches), the server view is adopted wholesale and that one conflicting edit is dropped. This is rare and only happens after a structural change from elsewhere.
Forcing an immediate sync
client.flushContextSync();Use this before navigating away or closing the tab, when you cannot afford to wait the 100 ms.
window.addEventListener('beforeunload', () => {
client.flushContextSync();
});Observing context changes
client.on('contextUpdated', ({ context, patches }) => {
console.log('patches sent:', patches);
});The patches array is the RFC 6902 ops that were actually sent — useful for debugging "why did the server not see my update?".
With the Pinia store
If you are using useAutopilotStore(), its context property is a reactive object you can mutate directly: store.context.foo = 'bar'. A deep watcher auto-syncs mutations to the client (same 100 ms debounce). The convenience actions — store.setContextProperty('foo', 'bar') — still work and do the same thing. See the store reference.