// Client data layer: apply server events and cache API reads into a Solid store. // Prefer straightforward projection. Do not add generation counters, stale-response // merges, live/history overlays, or other race machinery here—last write wins. // Reconnect invalidates cached reads; active UI owners decide what to sync again. import type { AgentInfo, CommandInfo, FormInfo, IntegrationInfo, LocationRef, LocationGetOutput, McpResource, McpServer, ModelInfo, ModelRef, PermissionSavedInfo, PermissionRequest, PermissionReplyInput, Project, ProviderInfo, ReferenceInfo, SessionMessageInfo, SessionMessageAssistant, SessionMessageAssistantReasoning, SessionMessageAssistantText, SessionMessageAssistantTool, SessionInfo, SessionInboxInfo, ShellInfo, SkillInfo, VcsInfo, OpenCodeEvent, OpenCodeClient, WebSearchProvider, } from "../promise" import { Worktree } from "@opencode-ai/schema/worktree" import { SessionID } from "@opencode-ai/schema/session-id" import { SessionMessage } from "@opencode-ai/schema/session-message" import { isPermissionNotFoundError, type SessionPromptInput } from "../promise" import { createStore, produce, reconcile } from "solid-js/store" import type { SessionInbox } from "@opencode-ai/schema/session-inbox" import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js" export type DataSessionStatus = "idle" | "running" export type CreateDataInput = { readonly api: () => OpenCodeClient readonly directory: string readonly event: { readonly on: ( type: Type, handler: (event: Extract) => void, ) => () => void readonly listen: (handler: (event: { name: OpenCodeEvent["type"]; details: OpenCodeEvent }) => void) => () => void } readonly connection?: { readonly status: () => "connected" | "connecting" | "reconnecting" } } const messageIDFromEvent = (eventID: string) => eventID.replace(/^evt_/, "msg_") // Global MCP elicitations temporarily use "global" instead of a real session ID, so the // server cannot recover their Location when settling them. Preserve the event Location // until MCP elicitations carry session ownership. export type FormWithLocation = FormInfo & { readonly location?: LocationRef } type ShellWithLocation = ShellInfo & { readonly location: LocationRef } type LocationData = { info?: LocationGetOutput vcs?: VcsInfo agent?: AgentInfo[] command?: CommandInfo[] integration?: IntegrationInfo[] mcp?: { server?: McpServer[] resource?: McpResource[] } model?: ModelInfo[] provider?: ProviderInfo[] reference?: ReferenceInfo[] websearch?: WebSearchProvider[] // Currently running shell commands for this location, keyed by shell id. Entries are removed // once the command exits or is deleted, so this only ever holds in-flight shells. shell?: Record skill?: SkillInfo[] } type Store = { session: { info: Record // Family index keyed by a family's root (or furthest-known-ancestor when the // true root is not yet loaded). The value is a flat deduplicated list of every // session ID in that family, including the key itself once its info arrives. family: Record active: Record message: Record messageCursor: Record messageLoading: Record pending: Record input: Record permission: Record // Pending forms keyed by owner: a session ID or the temporary "global" elicitation sentinel. form: Record } project: { info: Record permission: Record } location: Record } export function locationKey(location: LocationRef) { return JSON.stringify([location.directory, location.workspaceID]) } function locationQuery(ref?: LocationRef) { return ref ? { directory: ref.directory, workspace: ref.workspaceID } : undefined } function createSync() { const state = new Map>() return { run(key: string, load: () => Promise) { const active = state.get(key) if (active === true) return Promise.resolve() if (active) return active const pending = load() .then(() => { if (state.get(key) === pending) state.set(key, true) }) .finally(() => { if (state.get(key) === pending) state.delete(key) }) state.set(key, pending) return pending }, complete(key: string) { if (state.has(key)) return state.set(key, true) }, invalidate(key?: string) { if (key) { state.delete(key) return } state.clear() }, } } export function createData(config: CreateDataInput) { const api = config.api const [store, setStore] = createStore({ session: { info: {}, family: {}, active: {}, message: {}, messageCursor: {}, messageLoading: {}, pending: {}, input: {}, permission: {}, form: {}, }, project: { info: {}, permission: {}, }, location: {}, }) const [defaultLocation, setDefaultLocation] = createSignal({ directory: config.directory }) const sessions = createMemo(() => Object.values(store.session.info).toSorted((a, b) => b.time.updated - a.time.updated), ) const messageIndex = new Map>() const sync = createSync() function setSessionActive(sessionID: string, status: DataSessionStatus) { setStore("session", "active", sessionID, status) } function removePending(sessionID: string, inboxID?: string) { if (!inboxID) return if (store.session.pending[sessionID]?.some((item) => item.id === inboxID)) setStore( "session", "pending", sessionID, (store.session.pending[sessionID] ?? []).filter((item) => item.id !== inboxID), ) if (store.session.input[sessionID]?.includes(inboxID)) setStore( "session", "input", sessionID, (store.session.input[sessionID] ?? []).filter((id) => id !== inboxID), ) } function removePermission(sessionID: string, requestID: string) { const requests = store.session.permission[sessionID] if (!requests?.some((request) => request.id === requestID)) return setStore( "session", "permission", sessionID, requests.filter((request) => request.id !== requestID), ) } function updatePending(sessionID: string, inboxID: string, delivery: SessionInbox.Delivery) { const index = store.session.pending[sessionID]?.findIndex((item) => item.id === inboxID) ?? -1 const item = store.session.pending[sessionID]?.[index] if (index < 0 || !item || item.delivery === delivery) return setStore("session", "pending", sessionID, index, { ...item, delivery }) } // Inbox IDs of optimistic prompt admissions still awaiting their durable // echo. This is the one deliberate piece of in-flight bookkeeping in this // layer: it exists so a rejection only rolls back rows the server never // acknowledged, and so a concurrent pending re-fetch cannot wipe a row the // server does not know about yet. Entries clear on the enqueued echo or on // rollback — not on POST success, which typically precedes the echo. const outbox = new Set() // Session IDs of optimistic create admissions still awaiting acknowledgement // (the session.created echo or the create response itself). A failed create // only rolls back a session the server never acknowledged. Unlike // `creating`, this clears on the echo rather than request settlement. const sessionOutbox = new Set() // In-flight optimistic creates by session ID. prompt() gates its POST on // this so a prompt sent to a still-creating session waits for the session // to exist server-side instead of failing with "not found". const creating = new Map>() // Per-session send chain: prompts must be admitted in submission order, // and HTTP gives no ordering across concurrent POSTs. Each prompt waits // for the previous prompt's POST (settled, so one failure does not block // the next) before sending its own. const sending = new Map>() // Register `promise` under `key` until it settles. A later registration // replaces an earlier one; settlement only clears its own entry. function track(map: Map>, key: string, promise: Promise) { map.set(key, promise) const settle = () => { if (map.get(key) === promise) map.delete(key) } void promise.then(settle, settle) } // Upsert an admitted inbox item into pending, input, and (for user and // synthetic items) the visible transcript. Used by the inbox.enqueued // handler and by optimistic prompt admission; the upsert is what reconciles // the durable echo with an optimistic placeholder — the durable payload and // times replace the client's guess. function admitLocal(item: SessionInboxInfo) { batch(() => { const pending = store.session.pending[item.sessionID] ?? [] const at = pending.findIndex((entry) => entry.id === item.id) setStore( "session", "pending", item.sessionID, at < 0 ? [...pending, item] : pending.map((entry, index) => (index === at ? item : entry)), ) const input = store.session.input[item.sessionID] ?? [] if (!input.includes(item.id)) setStore("session", "input", item.sessionID, [...input, item.id]) materializeInboxMessage(item) }) } function materializeInboxMessage(item: SessionInboxInfo) { if (item.type !== "user" && item.type !== "synthetic") return message.update(item.sessionID, (draft, index) => { const row = item.type === "user" ? { id: item.id, type: "user" as const, ...item.payload, time: { created: item.timeCreated } } : { id: item.id, type: "synthetic" as const, ...item.payload, time: { created: item.timeCreated } } const position = index.get(item.id) if (position === undefined) return message.append(draft, index, row) draft[position] = row }) } // Remove an inbox item from pending, input, and the visible transcript. // Used by the inbox.cancelled handler and by optimistic rollback. function retractLocal(sessionID: string, inboxID: string) { batch(() => { removePending(sessionID, inboxID) if (!messageIndex.get(sessionID)?.has(inboxID)) return message.update(sessionID, (draft, index) => { const position = index.get(inboxID) if (position === undefined) return draft.splice(position, 1) index.delete(inboxID) message.reindex(draft, index, position) }) }) } const message = { update(sessionID: string, fn: (messages: SessionMessageInfo[], index: Map) => void) { setStore( "session", "message", produce((draft) => { fn((draft[sessionID] ??= []), index(sessionID)) }), ) }, append(messages: SessionMessageInfo[], index: Map, item: SessionMessageInfo) { if (index.has(item.id)) return index.set(item.id, messages.length) messages.push(item) }, activeAssistant(messages: SessionMessageInfo[]) { const item = messages.findLast((item) => item.type === "assistant" && !item.time.completed) return item?.type === "assistant" ? item : undefined }, assistant(messages: SessionMessageInfo[], index: Map, messageID: string) { const position = index.get(messageID) const item = position === undefined ? undefined : messages[position] return item?.type === "assistant" ? item : undefined }, shell(messages: SessionMessageInfo[], shellID: string) { const item = messages.findLast((item) => item.type === "shell" && item.shellID === shellID) return item?.type === "shell" ? item : undefined }, compaction(messages: SessionMessageInfo[]) { const item = messages.findLast((item) => item.type === "compaction" && item.status === "running") return item?.type === "compaction" ? item : undefined }, latestTool(assistant: SessionMessageAssistant | undefined, id?: string) { return assistant?.content.findLast( (item): item is SessionMessageAssistantTool => item.type === "tool" && (id === undefined || item.id === id), ) }, latestText(assistant: SessionMessageAssistant | undefined) { return assistant?.content.findLast((item): item is SessionMessageAssistantText => item.type === "text") }, latestReasoning(assistant: SessionMessageAssistant | undefined) { return assistant?.content.findLast( (item): item is SessionMessageAssistantReasoning => item.type === "reasoning" && !item.time?.completed, ) }, reindex(messages: SessionMessageInfo[], index: Map, start: number) { for (let position = start; position < messages.length; position++) { const item = messages[position] if (item) index.set(item.id, position) } }, } function index(sessionID: string) { const existing = messageIndex.get(sessionID) if (existing) return existing const created = new Map() messageIndex.set(sessionID, created) return created } // Walk parentID upward through loaded session info to the family root. When a // parent's info is missing, that missing ID is the furthest-known ancestor and // is returned so orphan subtrees group under it until the parent arrives. A // seen set guards against parent cycles, stopping at the last non-repeating // ancestor. function resolveRoot(sessionID: string) { let current = sessionID let parentID = store.session.info[sessionID]?.parentID const seen = new Set([sessionID]) while (parentID) { if (seen.has(parentID)) break seen.add(parentID) current = parentID parentID = store.session.info[parentID]?.parentID } return current } // Register one session into the family index. Idempotent: syncing an // existing session never duplicates its ID. When a tentative family keyed by // sessionID exists (descendants arrived while sessionID's own info was // absent) but sessionID turns out to have a parent, fold the orphan subtree // into the resolved root's family and drop the tentative entry. function registerSession(sessionID: string) { const info = store.session.info[sessionID] if (!info) return const rootID = resolveRoot(sessionID) setStore( "session", "family", produce((draft) => { if (sessionID !== rootID && draft[sessionID]) { const members = (draft[rootID] ??= []) for (const id of draft[sessionID]) { if (!members.includes(id)) members.push(id) } delete draft[sessionID] } const family = (draft[rootID] ??= []) if (!family.includes(sessionID)) family.push(sessionID) }), ) } function removeSession(sessionID: string) { store.session.pending[sessionID]?.forEach((item) => outbox.delete(item.id)) messageIndex.delete(sessionID) sync.invalidate(`session:${sessionID}`) sync.invalidate(`session.family:${sessionID}`) sync.invalidate(`session.pending:${sessionID}`) sync.invalidate(`session.message:${sessionID}`) sync.invalidate(`session.permission:${sessionID}`) sync.invalidate(`session.form:${sessionID}:`) setStore( "session", produce((draft) => { delete draft.info[sessionID] delete draft.active[sessionID] delete draft.message[sessionID] delete draft.messageCursor[sessionID] delete draft.messageLoading[sessionID] delete draft.pending[sessionID] delete draft.input[sessionID] delete draft.permission[sessionID] delete draft.form[sessionID] for (const [rootID, family] of Object.entries(draft.family)) { const next = family.filter((id) => id !== sessionID) if (next.length === 0) delete draft.family[rootID] else draft.family[rootID] = next } }), ) } function handleEvent(event: OpenCodeEvent) { switch (event.type) { case "server.connected": void api() .session.active() .then((active) => { setStore( "session", "active", reconcile(Object.fromEntries(Object.keys(active).map((sessionID) => [sessionID, "running" as const]))), ) }) .catch(() => undefined) void api() .location.get({ location: locationQuery(defaultLocation()) }) .then((location) => { const key = locationKey(location) setStore("location", key, { ...store.location[key], info: location }) }) .catch((error) => console.error("Failed to preload location", error)) void result.location.vcs.sync().catch((error) => console.error("Failed to preload VCS info", error)) void result.project.sync().catch((error) => console.error("Failed to preload projects", error)) return case "session.created": sessionOutbox.delete(event.data.sessionID) result.session.invalidate(event.data.sessionID) void result.session.sync(event.data.sessionID) // Band-aid: a newly created session starts empty, so live events can be its source of truth. // Fetching pending inputs and projected messages separately lets promotion move an input between snapshots, // causing both requests to miss it and overwrite event-built state. Skip those racy initial reads until // hydration can load pending and projected messages atomically. sync.complete(`session.pending:${event.data.sessionID}`) sync.complete(`session.message:${event.data.sessionID}`) return case "session.deleted": removeSession(event.data.sessionID) return case "session.usage.updated": if (store.session.info[event.data.sessionID]) setStore("session", "info", event.data.sessionID, { cost: event.data.cost, tokens: event.data.tokens, }) return case "session.agent.selected": { const previous = store.session.info[event.data.sessionID]?.agent if (store.session.info[event.data.sessionID]) setStore("session", "info", event.data.sessionID, "agent", event.data.agent) message.update(event.data.sessionID, (draft, index) => { message.append(draft, index, { id: messageIDFromEvent(event.id), type: "agent-switched", agent: event.data.agent, previous, time: { created: event.created }, }) }) return } case "session.model.selected": if (store.session.info[event.data.sessionID]) setStore("session", "info", event.data.sessionID, "model", event.data.model) if (!store.session.message[event.data.sessionID]) return message.update(event.data.sessionID, (draft, index) => { message.append(draft, index, { id: messageIDFromEvent(event.id), type: "model-switched", model: event.data.model, time: { created: event.created }, }) }) void api() .session.message({ sessionID: event.data.sessionID, messageID: messageIDFromEvent(event.id) }) .then((item) => { message.update(event.data.sessionID, (draft, index) => { const position = index.get(item.id) if (position === undefined) return message.append(draft, index, item) draft[position] = item }) }) .catch((error) => console.error("Failed to load projected model switch message", error)) return case "session.renamed": // Preserve the live title when it races the session's initial read. void result.session.sync(event.data.sessionID).then(() => { if (store.session.info[event.data.sessionID]) setStore("session", "info", event.data.sessionID, "title", event.data.title) }) return case "session.moved": { const current = store.session.info[event.data.sessionID] if (current) { const previous = { location: { ...current.location }, projectID: current.projectID, subpath: current.subpath, } setStore("session", "info", event.data.sessionID, "location", event.data.location) if (event.data.projectID) setStore("session", "info", event.data.sessionID, "projectID", event.data.projectID) setStore("session", "info", event.data.sessionID, "subpath", event.data.subpath) message.update(event.data.sessionID, (draft, index) => { message.append(draft, index, { id: messageIDFromEvent(event.id), type: "location-switched", location: event.data.location, projectID: event.data.projectID, subpath: event.data.subpath, previous, time: { created: event.created }, }) }) } return } case "worktree.resolved": { for (const [sessionID, info] of Object.entries(store.session.info)) { const adopted = Worktree.adopt({ projectID: info.projectID, directory: info.location.directory }, event.data) if (!adopted) continue setStore("session", "info", sessionID, "projectID", adopted.projectID) setStore("session", "info", sessionID, "subpath", adopted.subpath) } return } case "session.inbox.delivered": { const admitted = store.session.input[event.data.sessionID]?.includes(event.data.inboxID) ?? false removePending(event.data.sessionID, event.data.inboxID) message.update(event.data.sessionID, (draft, index) => { const position = index.get(event.data.inboxID) if (position === undefined) return const existing = draft[position] if (!existing || !admitted) return existing.time.created = event.created draft.splice(position, 1) draft.push(existing) message.reindex(draft, index, position) }) return } case "session.inbox.delivery.changed": updatePending(event.data.sessionID, event.data.inboxID, event.data.delivery) return case "session.inbox.cancelled": { retractLocal(event.data.sessionID, event.data.inboxID) return } case "session.inbox.enqueued": { outbox.delete(event.data.inboxID) admitLocal({ id: event.data.inboxID, sessionID: event.data.sessionID, timeCreated: event.created, ...event.data.item, }) return } case "session.instructions.updated": // Mirror the projector: the initial baseline and empty-rendering deltas carry no text // and produce no transcript message. const updateText = event.data.text if (updateText === undefined) return message.update(event.data.sessionID, (draft, index) => { message.append(draft, index, { id: messageIDFromEvent(event.id), type: "system", text: updateText, description: `Instructions updated: ${Object.keys(event.data.delta).join(", ")}`, metadata: event.metadata, time: { created: event.created }, }) }) return case "session.synthetic": message.update(event.data.sessionID, (draft, index) => { message.append(draft, index, { id: messageIDFromEvent(event.id), type: "synthetic", text: event.data.text, description: event.data.description, metadata: event.data.metadata, time: { created: event.created }, }) }) return case "session.shell.started": message.update(event.data.sessionID, (draft, index) => { message.append(draft, index, { id: messageIDFromEvent(event.id), type: "shell", shellID: event.data.shell.id, command: event.data.shell.command, status: event.data.shell.status, exit: event.data.shell.exit, metadata: event.metadata, time: { created: event.created }, }) }) return case "session.shell.ended": message.update(event.data.sessionID, (draft) => { const match = message.shell(draft, event.data.shell.id) if (!match) return match.status = event.data.shell.status match.exit = event.data.shell.exit match.output = event.data.output match.time.completed = event.created }) return case "session.step.started": message.update(event.data.sessionID, (draft, index) => { const position = index.get(event.data.assistantMessageID) const existing = position === undefined ? undefined : draft[position] if (existing?.type === "assistant") { existing.agent = event.data.agent existing.model = event.data.model existing.retry = undefined existing.error = undefined existing.finish = undefined existing.rawFinish = undefined existing.providerState = undefined existing.time.completed = undefined if (event.data.snapshot) existing.snapshot = { ...existing.snapshot, start: event.data.snapshot } return } const currentAssistant = message.activeAssistant(draft) if (currentAssistant) { currentAssistant.retry = undefined currentAssistant.time.completed = event.created } message.append(draft, index, { id: event.data.assistantMessageID, type: "assistant", agent: event.data.agent, model: event.data.model, metadata: event.metadata, content: [], snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined, time: { created: event.created }, }) }) return case "session.step.ended": { message.update(event.data.sessionID, (draft, index) => { const currentAssistant = message.assistant(draft, index, event.data.assistantMessageID) if (!currentAssistant) return currentAssistant.time.completed = event.created currentAssistant.finish = event.data.finish currentAssistant.rawFinish = event.data.rawFinish currentAssistant.providerState = event.data.providerState currentAssistant.cost = event.data.cost currentAssistant.tokens = event.data.tokens if (event.data.snapshot) currentAssistant.snapshot = { ...currentAssistant.snapshot, end: event.data.snapshot } }) return } case "session.step.failed": message.update(event.data.sessionID, (draft, index) => { const currentAssistant = message.assistant(draft, index, event.data.assistantMessageID) if (!currentAssistant) return currentAssistant.time.completed = event.created currentAssistant.finish = event.data.finish ?? "error" currentAssistant.rawFinish = event.data.rawFinish currentAssistant.providerState = event.data.providerState currentAssistant.error = event.data.error currentAssistant.retry = undefined if (event.data.cost !== undefined && event.data.tokens !== undefined) { currentAssistant.cost = event.data.cost currentAssistant.tokens = event.data.tokens } }) return case "session.text.started": message.update(event.data.sessionID, (draft, index) => { message.assistant(draft, index, event.data.assistantMessageID)?.content.push({ type: "text", text: "", }) }) return case "session.text.delta": message.update(event.data.sessionID, (draft, index) => { const match = message.latestText(message.assistant(draft, index, event.data.assistantMessageID)) if (match) match.text += event.data.delta }) return case "session.text.ended": message.update(event.data.sessionID, (draft, index) => { const match = message.latestText(message.assistant(draft, index, event.data.assistantMessageID)) if (match) match.text = event.data.text }) return case "session.tool.input.started": message.update(event.data.sessionID, (draft, index) => { message.assistant(draft, index, event.data.assistantMessageID)?.content.push({ type: "tool", id: event.data.id, name: event.data.name, time: { created: event.created }, state: { status: "streaming", input: "" }, }) }) return case "session.tool.input.delta": message.update(event.data.sessionID, (draft, index) => { const match = message.latestTool( message.assistant(draft, index, event.data.assistantMessageID), event.data.id, ) if (match?.state.status === "streaming") match.state.input += event.data.delta }) return case "session.tool.input.ended": message.update(event.data.sessionID, (draft, index) => { const match = message.latestTool( message.assistant(draft, index, event.data.assistantMessageID), event.data.id, ) if (match?.state.status === "streaming") match.state.input = event.data.text }) return case "session.tool.called": message.update(event.data.sessionID, (draft, index) => { const match = message.latestTool( message.assistant(draft, index, event.data.assistantMessageID), event.data.id, ) if (!match) return match.time.ran = event.created match.executed = event.data.executed match.providerState = event.data.state match.state = { status: "running", input: event.data.input, metadata: {} } }) return case "session.tool.progress": message.update(event.data.sessionID, (draft, index) => { const match = message.latestTool( message.assistant(draft, index, event.data.assistantMessageID), event.data.id, ) if (match?.state.status !== "running") return match.state.metadata = event.data.metadata }) return case "session.tool.success": message.update(event.data.sessionID, (draft, index) => { const match = message.latestTool( message.assistant(draft, index, event.data.assistantMessageID), event.data.id, ) if (match?.state.status !== "running") return match.state = { status: "completed", input: match.state.input, metadata: event.data.metadata, content: [...event.data.content], } match.executed = event.data.executed || match.executed === true match.providerResultState = event.data.resultState match.time.completed = event.created }) return case "session.tool.failed": message.update(event.data.sessionID, (draft, index) => { const match = message.latestTool( message.assistant(draft, index, event.data.assistantMessageID), event.data.id, ) if (!match || (match.state.status !== "streaming" && match.state.status !== "running")) return match.state = { status: "error", error: event.data.error, input: typeof match.state.input === "string" ? {} : match.state.input, metadata: event.data.metadata, content: event.data.content, } match.executed = event.data.executed || match.executed === true match.providerResultState = event.data.resultState match.time.completed = event.created }) return case "session.reasoning.started": message.update(event.data.sessionID, (draft, index) => { message.assistant(draft, index, event.data.assistantMessageID)?.content.push({ type: "reasoning", text: "", state: event.data.state, time: { created: event.created }, }) }) return case "session.reasoning.delta": message.update(event.data.sessionID, (draft, index) => { const match = message.latestReasoning(message.assistant(draft, index, event.data.assistantMessageID)) if (match) match.text += event.data.delta }) return case "session.reasoning.ended": message.update(event.data.sessionID, (draft, index) => { const match = message.latestReasoning(message.assistant(draft, index, event.data.assistantMessageID)) if (match) { match.text = event.data.text match.time = { created: match.time?.created ?? event.created, completed: event.created } if (event.data.state !== undefined) match.state = event.data.state } }) return case "session.retry.scheduled": message.update(event.data.sessionID, (draft, index) => { const currentAssistant = message.assistant(draft, index, event.data.assistantMessageID) if (!currentAssistant) return currentAssistant.retry = { attempt: event.data.attempt, at: event.data.at, error: event.data.error, } }) return case "session.execution.started": setSessionActive(event.data.sessionID, "running") return case "session.compaction.started": if (event.data.inputID) removePending(event.data.sessionID, event.data.inputID) message.update(event.data.sessionID, (draft, index) => { message.append(draft, index, { id: event.data.inputID ?? messageIDFromEvent(event.id), type: "compaction", status: "running", reason: event.data.reason, summary: "", recent: event.data.recent ?? "", time: { created: event.created }, }) }) return case "session.execution.succeeded": case "session.execution.failed": case "session.execution.interrupted": setSessionActive(event.data.sessionID, "idle") message.update(event.data.sessionID, (draft) => { const currentAssistant = message.activeAssistant(draft) if (currentAssistant) currentAssistant.retry = undefined }) return case "session.revert.staged": if (store.session.info[event.data.sessionID]) setStore("session", "info", event.data.sessionID, "revert", event.data.revert) return case "session.revert.cleared": if (store.session.info[event.data.sessionID]) setStore("session", "info", event.data.sessionID, "revert", undefined) return case "session.revert.committed": if (store.session.info[event.data.sessionID]) { setStore("session", "info", event.data.sessionID, "revert", undefined) } setStore( "session", "input", event.data.sessionID, (store.session.input[event.data.sessionID] ?? []).filter((id) => id < event.data.to), ) message.update(event.data.sessionID, (draft, index) => { const position = draft.findIndex((item) => item.id >= event.data.to) if (position === -1) return for (const item of draft.splice(position)) index.delete(item.id) }) return case "session.compaction.delta": message.update(event.data.sessionID, (draft) => { const current = message.compaction(draft) if (current?.status === "running") current.summary += event.data.text }) return case "session.compaction.ended": message.update(event.data.sessionID, (draft, index) => { const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running") const current = draft[position] if (current?.type === "compaction") { Object.assign(current, { status: "completed", reason: event.data.reason, summary: event.data.text, recent: event.data.recent, }) return } message.append(draft, index, { id: messageIDFromEvent(event.id), type: "compaction", status: "completed", reason: event.data.reason, summary: event.data.text, recent: event.data.recent, time: { created: event.created }, }) }) return case "session.compaction.failed": if (event.data.inputID) removePending(event.data.sessionID, event.data.inputID) message.update(event.data.sessionID, (draft, index) => { const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running") const current = draft[position] const failed: Extract = { id: current?.id ?? event.data.inputID ?? messageIDFromEvent(event.id), type: "compaction", status: "failed", reason: event.data.reason ?? "manual", error: event.data.error ?? { type: "compaction.failed", message: "Compaction failed before recording an error", }, metadata: current?.type === "compaction" ? current.metadata : event.metadata, time: current?.type === "compaction" ? current.time : { created: event.created }, } if (current?.type === "compaction") { draft[position] = failed return } message.append(draft, index, failed) }) return case "permission.asked": if (store.session.permission[event.data.sessionID]?.some((request) => request.id === event.data.id)) return setStore("session", "permission", event.data.sessionID, [ ...(store.session.permission[event.data.sessionID] ?? []), event.data, ]) return case "permission.replied": removePermission(event.data.sessionID, event.data.requestID) return case "form.replied": case "form.cancelled": setStore( "session", "form", event.data.sessionID, (store.session.form[event.data.sessionID] ?? []).filter((form) => form.id !== event.data.id), ) return } if (!event.location) return const location = event.location switch (event.type) { case "catalog.updated": result.location.model.invalidate(location) result.location.provider.invalidate(location) void Promise.all([result.location.model.sync(location), result.location.provider.sync(location)]) break case "agent.updated": result.location.agent.invalidate(location) void result.location.agent.sync(location) break case "command.updated": result.location.command.invalidate(location) void result.location.command.sync(location) break case "skill.updated": result.location.skill.invalidate(location) void result.location.skill.sync(location) break case "vcs.branch.updated": setStore("location", locationKey(location), (data) => ({ ...data, vcs: { branch: { ...data?.vcs?.branch, current: event.data.branch, }, }, })) break case "form.created": if (store.session.form[event.data.form.sessionID]?.some((form) => form.id === event.data.form.id)) break setStore("session", "form", event.data.form.sessionID, [ ...(store.session.form[event.data.form.sessionID] ?? []), event.data.form.sessionID === "global" ? { ...event.data.form, location } : event.data.form, ]) break case "shell.created": setStore("location", locationKey(location), (data) => ({ ...data, shell: { ...data?.shell, [event.data.info.id]: { ...event.data.info, location }, }, })) break case "shell.exited": case "shell.deleted": setStore("location", locationKey(location), (data) => ({ ...data, shell: Object.fromEntries(Object.entries(data?.shell ?? {}).filter(([id]) => id !== event.data.id)), })) break case "reference.updated": result.location.reference.invalidate() void result.location.reference.sync() break case "integration.updated": result.location.integration.invalidate(location) result.location.model.invalidate(location) result.location.provider.invalidate(location) void Promise.all([ result.location.integration.sync(location), result.location.model.sync(location), result.location.provider.sync(location), ]) break case "config.updated": case "websearch.updated": void result.location.websearch.refresh(location) break // Authenticating an MCP integration reconnects its server, which emits mcp.status.changed, // so the mcp list syncs here rather than off integration.updated. case "mcp.status.changed": result.location.mcp.server.invalidate(location) void result.location.mcp.server.sync(location) break case "mcp.resources.changed": result.location.mcp.resource.invalidate(location) void result.location.mcp.resource.sync(location) break } } const result = { on: config.event.on, listen: config.event.listen, session: { list() { return sessions() }, get(sessionID: string) { return store.session.info[sessionID] }, remember(info: SessionInfo) { setStore("session", "info", info.id, reconcile(info)) sync.complete(`session:${info.id}`) registerSession(info.id) }, setStatus(sessionID: string, status: DataSessionStatus) { setSessionActive(sessionID, status) }, root(sessionID: string) { return resolveRoot(sessionID) }, family(sessionID: string) { return store.session.family[resolveRoot(sessionID)] ?? [] }, cost(sessionID: string) { const session = store.session.info[sessionID] if (!session) return 0 if (session.parentID) return session.cost return (store.session.family[sessionID] ?? [sessionID]).reduce( (total, id) => total + (store.session.info[id]?.cost ?? 0), 0, ) }, status(sessionID: string) { return store.session.active[sessionID] ?? "idle" }, input: { list(sessionID: string) { return store.session.input[sessionID] ?? [] }, has(sessionID: string, inboxID: string) { return store.session.input[sessionID]?.includes(inboxID) ?? false }, }, pending: { list(sessionID: string) { return store.session.pending[sessionID] ?? [] }, sync(sessionID: string) { return sync.run(`session.pending:${sessionID}`, async () => { const pending = await api().session.inbox.list({ sessionID }) // Keep optimistic rows still awaiting their echo: this fetch may // have raced ahead of an in-flight admission the server does not // know about yet. const inflight = (store.session.pending[sessionID] ?? []).filter( (item) => outbox.has(item.id) && !pending.some((row) => row.id === item.id), ) const merged = inflight.length === 0 ? pending : [...pending, ...inflight] batch(() => { setStore("session", "pending", sessionID, reconcile(merged)) setStore( "session", "input", sessionID, reconcile(merged.filter((item) => item.type !== "compaction").map((item) => item.id)), ) merged.forEach(materializeInboxMessage) }) }) }, invalidate(sessionID: string) { sync.invalidate(`session.pending:${sessionID}`) }, }, // Optimistic session creation: admit a local record under a // client-minted ID so a session view can mount immediately, then create // the session on the server. The session.created echo re-syncs the // record by ID, so the durable payload replaces the client's guess. // Returns the ID synchronously along with the in-flight request: // callers gate session-dependent sends on the request (prompt() gates // itself on any in-flight create of its session automatically). create(input: { id?: string title?: string agent?: string model?: ModelRef location?: LocationRef projectID?: string }) { const { projectID, ...payload } = input const id = payload.id ?? SessionID.create() const location = payload.location ?? defaultLocation() const fresh = !store.session.info[id] if (fresh) { const now = Date.now() sessionOutbox.add(id) result.session.remember({ id, projectID: projectID ?? store.location[locationKey(location)]?.info?.project.id ?? "", agent: payload.agent, model: payload.model, cost: 0, tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, time: { created: now, updated: now }, title: payload.title, location, }) // A mounted optimistic session must not fetch its empty collections // before creation settles. The session.created echo re-syncs info. sync.complete(`session.family:${id}`) sync.complete(`session.pending:${id}`) sync.complete(`session.message:${id}`) } // Wrapped so even a synchronous client failure reaches the rollback. const request = Promise.resolve() .then(() => api().session.create({ ...payload, id, location })) .then((info) => { sessionOutbox.delete(id) result.session.remember(info) return info }) .catch((error) => { // Roll back only a record this call admitted and neither the echo // nor the response has acknowledged: anything else is server state. if (fresh && sessionOutbox.delete(id)) removeSession(id) throw error }) if (fresh) track(creating, id, request) return { id, request } }, // Optimistic prompt admission: render the prompt immediately under a // client-minted ID, send it, and let the durable inbox.enqueued echo // upsert that same ID with the server's payload. Server admission is // idempotent per ID, so retrying with the identical payload cannot // double-admit. prompt(input: SessionPromptInput & { gate?: Promise }) { const { gate, ...request } = input const id = request.id ?? SessionMessage.ID.create() // A retry may reuse an ID that is already rendered — and possibly // already durable. Admit optimistically only for new IDs so a failed // retry cannot roll back acknowledged state. const fresh = !messageIndex.get(request.sessionID)?.has(id) && !store.session.pending[request.sessionID]?.some((item) => item.id === id) if (fresh) { outbox.add(id) admitLocal({ id, sessionID: request.sessionID, timeCreated: Date.now(), type: "user", delivery: request.delivery ?? "steer", // Files and skills stay off the optimistic row: their durable // forms are server-loaded (content, mime, resolution), so they // fill in when the echo upserts the row. payload: { text: request.text, agents: request.agents?.map((agent) => ({ ...agent })), metadata: request.metadata, }, }) } // Wrapped so even a synchronous client failure reaches the rollback. // The POST additionally waits for the caller's gate, for any // in-flight optimistic create of this session, and for the previous // prompt's POST: the row renders now, the send happens once the // session exists server-side and earlier prompts are admitted. const previous = sending.get(request.sessionID) const send = Promise.resolve() .then(() => Promise.all([gate, creating.get(request.sessionID), previous])) .then(() => api().session.prompt({ ...request, id })) track( sending, request.sessionID, send.then( () => undefined, () => undefined, ), ) return send.catch((error) => { // Roll back only rows this call admitted and the echo has not // acknowledged: anything else is server state. if (fresh && outbox.delete(id)) retractLocal(request.sessionID, id) throw error }) }, sync(sessionID: string, options?: { children?: boolean }) { return sync.run(options?.children ? `session.family:${sessionID}` : `session:${sessionID}`, async () => { const [info, children] = await Promise.all([ api().session.get({ sessionID }), options?.children ? api() .session.list({ parentID: sessionID, order: "desc" }) .then((response) => response.data) : [], ]) const sessions = [info, ...children] setStore( "session", "info", produce((draft) => { for (const session of sessions) draft[session.id] = session }), ) for (const session of sessions) { sync.complete(`session:${session.id}`) registerSession(session.id) } }) }, invalidate(sessionID: string) { sync.invalidate(`session:${sessionID}`) }, message: { list(sessionID: string) { return store.session.message[sessionID] ?? [] }, get(sessionID: string, messageID: string) { const messages = store.session.message[sessionID] const position = messageIndex.get(sessionID)?.get(messageID) return position === undefined ? undefined : messages?.[position] }, sync(sessionID: string) { return sync.run(`session.message:${sessionID}`, async () => { const response = await api().message.list({ sessionID, limit: 200, order: "desc" }) const fetched = response.data.toReversed() // Same protection as the pending sync: a re-fetch racing an // admission must not wipe its local transcript row. const ids = new Set(fetched.map((item) => item.id)) const admitted = new Set( (store.session.pending[sessionID] ?? []).flatMap((item) => item.type === "user" || item.type === "synthetic" ? [item.id] : [], ), ) const local = (store.session.message[sessionID] ?? []).filter( (item) => !ids.has(item.id) && (outbox.has(item.id) || admitted.has(item.id)), ) const messages = local.length === 0 ? fetched : [...fetched, ...local] messageIndex.set(sessionID, new Map(messages.map((message, index) => [message.id, index]))) setStore("session", "message", sessionID, reconcile(messages)) setStore("session", "messageCursor", sessionID, response.cursor.next ?? undefined) }) }, more(sessionID: string) { return store.session.messageCursor[sessionID] !== undefined }, loading(sessionID: string) { return store.session.messageLoading[sessionID] ?? false }, async loadMore(sessionID: string) { const cursor = store.session.messageCursor[sessionID] if (!cursor || store.session.messageLoading[sessionID]) return setStore("session", "messageLoading", sessionID, true) const response = await api() .message.list({ sessionID, limit: 200, cursor }) .finally(() => setStore("session", "messageLoading", sessionID, false)) const older = response.data.toReversed() const existing = store.session.message[sessionID] ?? [] const ids = new Set(existing.map((item) => item.id)) const messages = [...older.filter((item) => !ids.has(item.id)), ...existing] messageIndex.set(sessionID, new Map(messages.map((item, position) => [item.id, position]))) setStore("session", "message", sessionID, reconcile(messages)) setStore("session", "messageCursor", sessionID, response.cursor.next ?? undefined) }, invalidate(sessionID: string) { sync.invalidate(`session.message:${sessionID}`) }, }, permission: { list(sessionID: string) { return store.session.permission[sessionID] }, sync(sessionID: string) { return sync.run(`session.permission:${sessionID}`, async () => { setStore("session", "permission", sessionID, await api().permission.list({ sessionID })) }) }, invalidate(sessionID: string) { sync.invalidate(`session.permission:${sessionID}`) }, async reply(input: PermissionReplyInput) { await api() .permission.reply(input) .catch((error: unknown) => { if (!isPermissionNotFoundError(error)) throw error }) removePermission(input.sessionID, input.requestID) }, }, form: { list(sessionID: string, ref?: LocationRef) { const forms = store.session.form[sessionID] if (sessionID !== "global") return forms if (!ref) return const key = locationKey(ref) return forms?.filter((form) => form.location && locationKey(form.location) === key) }, sync(sessionID: string, ref?: LocationRef) { const key = `session.form:${sessionID}:${sessionID === "global" ? locationKey(ref ?? defaultLocation()) : ""}` return sync.run(key, async () => { if (sessionID === "global") { const response = await api().form.request.list({ location: locationQuery(ref ?? defaultLocation()), }) const location = { directory: response.location.directory, workspaceID: response.location.workspaceID, } const locationID = locationKey(location) setStore("session", "form", sessionID, [ ...(store.session.form[sessionID] ?? []).filter( (form) => form.location && locationKey(form.location) !== locationID, ), ...response.data.filter((form) => form.sessionID === "global").map((form) => ({ ...form, location })), ]) return } setStore("session", "form", sessionID, await api().form.list({ sessionID })) }) }, invalidate(sessionID: string, ref?: LocationRef) { sync.invalidate( `session.form:${sessionID}:${sessionID === "global" ? locationKey(ref ?? defaultLocation()) : ""}`, ) }, }, }, project: { list() { return Object.values(store.project.info).toSorted((a, b) => b.time.updated - a.time.updated) }, get(projectID: string) { return store.project.info[projectID] }, sync() { return sync.run("project", async () => { const projects = await api().project.list() setStore("project", "info", reconcile(Object.fromEntries(projects.map((project) => [project.id, project])))) }) }, invalidate() { sync.invalidate("project") }, permission: { list(projectID: string) { return store.project.permission[projectID] }, sync(projectID: string) { return sync.run(`project.permission:${projectID}`, async () => { setStore("project", "permission", projectID, await api().permission.saved.list({ projectID })) }) }, invalidate(projectID: string) { sync.invalidate(`project.permission:${projectID}`) }, }, }, shell: { list(location?: LocationRef) { return Object.values(store.location[locationKey(location ?? defaultLocation())]?.shell ?? {}) }, listBySession(sessionID: string) { return Object.values(store.location) .flatMap((data) => Object.values(data.shell ?? {})) .filter((shell) => shell.metadata.sessionID === sessionID) }, get(id: string) { return Object.values(store.location) .map((data) => data.shell?.[id]) .find((shell) => shell !== undefined) }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.shell:${id}`, async () => { const response = await api().shell.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], shell: Object.fromEntries( response.data.map((info) => [ info.id, { ...info, location: { directory: response.location.directory, workspaceID: response.location.workspaceID, }, }, ]), ), }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.shell:${locationKey(ref ?? defaultLocation())}`) }, }, location: { info(ref?: LocationRef) { return store.location[locationKey(ref ?? defaultLocation())]?.info }, default() { return defaultLocation() }, syncInfo(ref?: LocationRef) { const current = ref ?? defaultLocation() return sync.run(`location:${locationKey(current)}`, async () => { const location = await api().location.get({ location: locationQuery(current) }) const key = locationKey(location) if (!store.location[key]) setStore("location", key, {}) setStore("location", key, "info", location) if (!ref) { setDefaultLocation({ directory: location.directory, workspaceID: location.workspaceID }) } }) }, async sync(ref?: LocationRef) { await result.location.syncInfo(ref) const location = ref ?? defaultLocation() await Promise.all([ result.location.vcs.sync(location), result.location.agent.sync(location), result.location.command.sync(location), result.location.integration.sync(location), result.location.mcp.server.sync(location), result.location.mcp.resource.sync(location), result.location.model.sync(location), result.location.provider.sync(location), result.location.reference.sync(location), result.location.skill.sync(location), result.shell.sync(location), result.session.form.sync("global", location), ]) }, invalidate(ref?: LocationRef) { const location = ref ?? defaultLocation() sync.invalidate(`location:${locationKey(location)}`) result.location.vcs.invalidate(location) result.location.agent.invalidate(location) result.location.command.invalidate(location) result.location.integration.invalidate(location) result.location.mcp.server.invalidate(location) result.location.mcp.resource.invalidate(location) result.location.model.invalidate(location) result.location.provider.invalidate(location) result.location.reference.invalidate(location) result.location.skill.invalidate(location) result.shell.invalidate(location) result.session.form.invalidate("global", location) }, vcs: { info(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.vcs }, sync(ref?: LocationRef) { const location = ref ?? defaultLocation() return sync.run(`location.vcs:${locationKey(location)}`, async () => { const response = await api().vcs.get({ location: locationQuery(location) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], vcs: response.data }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.vcs:${locationKey(ref ?? defaultLocation())}`) }, }, agent: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.agent }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.agent:${id}`, async () => { const response = await api().agent.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], agent: response.data }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.agent:${locationKey(ref ?? defaultLocation())}`) }, }, command: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.command }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.command:${id}`, async () => { const response = await api().command.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], command: response.data }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.command:${locationKey(ref ?? defaultLocation())}`) }, }, integration: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.integration }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.integration:${id}`, async () => { const response = await api().integration.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], integration: response.data }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.integration:${locationKey(ref ?? defaultLocation())}`) }, }, mcp: { server: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.mcp?.server }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.mcp.server:${id}`, async () => { const response = await api().mcp.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], mcp: { ...store.location[key]?.mcp, server: response.data }, }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.mcp.server:${locationKey(ref ?? defaultLocation())}`) }, }, resource: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.mcp?.resource }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.mcp.resource:${id}`, async () => { const response = await api().mcp.resource.catalog({ location: locationQuery(ref ?? defaultLocation()), }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], mcp: { ...store.location[key]?.mcp, resource: response.data.resources }, }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.mcp.resource:${locationKey(ref ?? defaultLocation())}`) }, }, }, model: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.model }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.model:${id}`, async () => { const response = await api().model.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], model: response.data }) if (key !== id) setStore("location", id, { ...store.location[id], model: response.data }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.model:${locationKey(ref ?? defaultLocation())}`) }, }, provider: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.provider }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.provider:${id}`, async () => { const response = await api().provider.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], provider: response.data }) if (key !== id) setStore("location", id, { ...store.location[id], provider: response.data }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.provider:${locationKey(ref ?? defaultLocation())}`) }, }, reference: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.reference }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.reference:${id}`, async () => { const response = await api().reference.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], reference: response.data }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.reference:${locationKey(ref ?? defaultLocation())}`) }, }, websearch: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.websearch }, async refresh(ref?: LocationRef) { const input = { location: locationQuery(ref ?? defaultLocation()) } const providers = await api().websearch.providers(input) const key = locationKey(providers.location) setStore("location", key, { ...store.location[key], websearch: providers.data, }) }, }, skill: { list(location?: LocationRef) { return store.location[locationKey(location ?? defaultLocation())]?.skill }, sync(ref?: LocationRef) { const id = locationKey(ref ?? defaultLocation()) return sync.run(`location.skill:${id}`, async () => { const response = await api().skill.list({ location: locationQuery(ref ?? defaultLocation()) }) const key = locationKey(response.location) setStore("location", key, { ...store.location[key], skill: response.data }) }) }, invalidate(ref?: LocationRef) { sync.invalidate(`location.skill:${locationKey(ref ?? defaultLocation())}`) }, }, }, } createEffect(() => { if (config.connection?.status() === "connected") return sync.invalidate() }) onCleanup( config.event.listen(({ details }) => { handleEvent(details) }), ) return result } export type Data = ReturnType