diff --git a/apps/web/components/task/dockview-desktop-layout.tsx b/apps/web/components/task/dockview-desktop-layout.tsx index d7b9afc0d5..3ce1c013fe 100644 --- a/apps/web/components/task/dockview-desktop-layout.tsx +++ b/apps/web/components/task/dockview-desktop-layout.tsx @@ -22,6 +22,7 @@ import { useLspFileOpener } from "@/hooks/use-lsp-file-opener"; import { useEditorKeybinds } from "@/hooks/use-editor-keybinds"; import { useSessionGitStatus } from "@/hooks/domains/session/use-session-git-status"; import { useSessionCommits } from "@/hooks/domains/session/use-session-commits"; +import { useEnvironmentSessionId } from "@/hooks/use-environment-session-id"; // Panel components (rendered via portals, not directly by dockview) import { TaskSessionSidebar } from "./task-session-sidebar"; @@ -77,7 +78,6 @@ import { PanelPortalHost, usePortalSlot } from "@/lib/layout/panel-portal-host"; * state that can't be swapped by simply reading a new `activeSessionId` from * the store: * - * - terminal — holds a live xterm WebSocket to a shell in the session's container * - file-editor — editing a file in the session's worktree * - browser — iframe preview of the session's dev server URL * - vscode — VS Code Server iframe running in the session's container @@ -90,12 +90,12 @@ import { PanelPortalHost, usePortalSlot } from "@/lib/layout/panel-portal-host"; * * - sidebar — workspace/task navigation, not session-specific * - chat — subscribes to `activeSessionId`, re-renders for new session - * - changes — reads session git status via `useSessionGitStatus(activeSessionId)` - * - files — reads the active session's file tree reactively + * - terminal — uses `useEnvironmentSessionId()`, reconnects only on env change + * - changes — uses `useEnvironmentSessionId()` for stable git state + * - files — uses `useEnvironmentSessionId()` for stable file tree * - plan — reads `activeTaskId` from the store */ const SESSION_SCOPED_COMPONENTS = new Set([ - "terminal", "file-editor", "browser", "vscode", @@ -263,10 +263,11 @@ function ChangesContent({ panelId }: { panelId: string }) { const addCommitDetailPanel = useDockviewStore((s) => s.addCommitDetailPanel); const { openFile } = useFileEditors(); - // Dynamic title with file count - const activeSessionId = useAppStore((state) => state.tasks.activeSessionId); + // Dynamic title with file count — use environment-stable sessionId so the + // tab title doesn't re-fetch on same-environment session tab switches. + const activeSessionId = useEnvironmentSessionId(); const gitStatus = useSessionGitStatus(activeSessionId); - const { commits } = useSessionCommits(activeSessionId ?? null); + const { commits } = useSessionCommits(activeSessionId); const fileCount = gitStatus?.files ? Object.keys(gitStatus.files).length : 0; const totalCount = fileCount + commits.length; @@ -468,7 +469,7 @@ export const DockviewDesktopLayout = memo(function DockviewDesktopLayout({ setupSessionTabSync(api, appStore); setupChatPanelSafetyNet(api, appStore); setupLayoutPersistence(api, saveTimerRef, sessionIdRef); - setupPortalCleanup(api); + setupPortalCleanup(api, appStore); }, [setApi, buildDefaultLayout, initialLayout, appStore], ); diff --git a/apps/web/components/task/dockview-layout-setup.ts b/apps/web/components/task/dockview-layout-setup.ts index 91900e215f..3b3dccf3fd 100644 --- a/apps/web/components/task/dockview-layout-setup.ts +++ b/apps/web/components/task/dockview-layout-setup.ts @@ -1,4 +1,6 @@ import type { DockviewReadyEvent } from "dockview-react"; +import type { StoreApi } from "zustand"; +import type { AppState } from "@/lib/state/store"; import { useDockviewStore } from "@/lib/state/dockview-store"; import { getRootSplitview } from "@/lib/state/dockview-layout-builders"; import { setSessionLayout } from "@/lib/local-storage"; @@ -73,7 +75,10 @@ export function setupLayoutPersistence( }); } -export function setupPortalCleanup(api: DockviewReadyEvent["api"]): void { +export function setupPortalCleanup( + api: DockviewReadyEvent["api"], + appStore: StoreApi, +): void { api.onDidRemovePanel((panel) => { if (useDockviewStore.getState().isRestoringLayout) return; const isMax = useDockviewStore.getState().preMaximizeLayout !== null; @@ -97,10 +102,13 @@ export function setupPortalCleanup(api: DockviewReadyEvent["api"]): void { }); } const entry = panelPortalManager.get(panel.id); + // vscode is session-scoped so entry.sessionId is always set by PortalSlot. if (entry?.component === "vscode" && entry.sessionId) stopVscode(entry.sessionId); - if (entry?.component === "terminal" && entry.sessionId) { + // terminal is global (no entry.sessionId) — fall back to the active session. + if (entry?.component === "terminal") { + const terminalSessionId = entry.sessionId ?? appStore.getState().tasks.activeSessionId; const terminalId = entry.params.terminalId as string | undefined; - if (terminalId) stopUserShell(entry.sessionId, terminalId); + if (terminalId && terminalSessionId) stopUserShell(terminalSessionId, terminalId); } panelPortalManager.release(panel.id); }); diff --git a/apps/web/hooks/domains/session/use-cumulative-diff.ts b/apps/web/hooks/domains/session/use-cumulative-diff.ts index d713ac76fc..39fd798e48 100644 --- a/apps/web/hooks/domains/session/use-cumulative-diff.ts +++ b/apps/web/hooks/domains/session/use-cumulative-diff.ts @@ -1,66 +1,105 @@ -import { useEffect, useCallback, useState } from "react"; +import { useEffect, useCallback, useRef, useState } from "react"; +import { useAppStore } from "@/components/state-provider"; import { getWebSocketClient } from "@/lib/ws/connection"; import type { CumulativeDiff } from "@/lib/state/slices/session-runtime/types"; const cumulativeDiffCache: Record = {}; const loadingState: Record = {}; -const listeners = new Set<(sessionId: string) => void>(); +const listeners = new Set<(envKey: string) => void>(); -export function invalidateCumulativeDiffCache(sessionId: string) { - delete cumulativeDiffCache[sessionId]; - listeners.forEach((fn) => fn(sessionId)); +/** + * Invalidate the cumulative diff cache for the given environment key. + * Callers should resolve sessionId → envKey before calling this. + */ +export function invalidateCumulativeDiffCache(envKey: string) { + delete cumulativeDiffCache[envKey]; + listeners.forEach((fn) => fn(envKey)); } export function useCumulativeDiff(sessionId: string | null) { + // Resolve to environment key so sessions sharing the same environment share the cache. + const envKey = useAppStore((state) => { + if (!sessionId) return null; + return state.environmentIdBySessionId[sessionId] ?? sessionId; + }); + + // Guard against stale responses after an environment switch. + const requestVersionRef = useRef(0); + const [diff, setDiff] = useState( - sessionId ? (cumulativeDiffCache[sessionId] ?? null) : null, + envKey ? (cumulativeDiffCache[envKey] ?? null) : null, ); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [invalidationCount, setInvalidationCount] = useState(0); const fetchCumulativeDiff = useCallback(async () => { - if (!sessionId) return; - if (loadingState[sessionId]) return; + if (!sessionId || !envKey) return; + if (loadingState[envKey]) return; const client = getWebSocketClient(); if (!client) return; + const version = ++requestVersionRef.current; + setLoading(true); - loadingState[sessionId] = true; + loadingState[envKey] = true; setError(null); try { + // Backend routes by session_id, but we cache by envKey const response = await client.request<{ cumulative_diff?: CumulativeDiff }>( "session.cumulative_diff", { session_id: sessionId }, ); + // Discard if the environment changed while the request was in flight + if (version !== requestVersionRef.current) return; + if (response?.cumulative_diff) { - cumulativeDiffCache[sessionId] = response.cumulative_diff; + cumulativeDiffCache[envKey] = response.cumulative_diff; setDiff(response.cumulative_diff); } } catch (err) { + if (version !== requestVersionRef.current) return; console.error("Failed to fetch cumulative diff:", err); setError(err instanceof Error ? err.message : "Failed to fetch cumulative diff"); } finally { - setLoading(false); - loadingState[sessionId] = false; + if (version === requestVersionRef.current) { + setLoading(false); + } + loadingState[envKey] = false; } - }, [sessionId]); + }, [sessionId, envKey]); + + // Sync cached state when envKey changes. Must run BEFORE the fetch effect + // so that fetchCumulativeDiff's setLoading(true) wins the React 18 batch. + useEffect(() => { + // Bump version so any in-flight fetch for the previous envKey is discarded. + // Clear the per-key loading flag so the fetch effect isn't blocked on re-entry + // (e.g. A→B→A where A's original fetch is still in-flight). + requestVersionRef.current++; + if (envKey) { + loadingState[envKey] = false; + setDiff(cumulativeDiffCache[envKey] ?? null); + } else { + setDiff(null); + } + setLoading(false); + }, [envKey]); // Fetch on mount and when cache is invalidated useEffect(() => { - if (!sessionId) return; + if (!envKey) return; fetchCumulativeDiff(); - }, [sessionId, invalidationCount, fetchCumulativeDiff]); + }, [envKey, invalidationCount, fetchCumulativeDiff]); // Subscribe to cache invalidation from WS handlers useEffect(() => { - if (!sessionId) return; - const handler = (invalidatedSessionId: string) => { - if (invalidatedSessionId === sessionId) { + if (!envKey) return; + const handler = (invalidatedEnvKey: string) => { + if (invalidatedEnvKey === envKey) { setInvalidationCount((c) => c + 1); } }; @@ -68,17 +107,7 @@ export function useCumulativeDiff(sessionId: string | null) { return () => { listeners.delete(handler); }; - }, [sessionId]); - - // Sync cached state when sessionId changes - useEffect(() => { - if (sessionId) { - setDiff(cumulativeDiffCache[sessionId] ?? null); - } else { - setDiff(null); - setLoading(false); - } - }, [sessionId]); + }, [envKey]); return { diff, diff --git a/apps/web/hooks/domains/session/use-session-commits.ts b/apps/web/hooks/domains/session/use-session-commits.ts index eea764f56d..962d6a61d1 100644 --- a/apps/web/hooks/domains/session/use-session-commits.ts +++ b/apps/web/hooks/domains/session/use-session-commits.ts @@ -5,15 +5,20 @@ import type { SessionCommit } from "@/lib/state/slices/session-runtime/types"; /** * Hook to fetch and manage commits for a session. - * Session commits track all commits made during a task session. + * Commits are keyed by environmentId so sessions sharing the same environment + * share the same commit list and don't duplicate fetches. */ export function useSessionCommits(sessionId: string | null) { - const commits = useAppStore((state) => - sessionId ? state.sessionCommits.bySessionId[sessionId] : undefined, - ); - const loading = useAppStore((state) => - sessionId ? state.sessionCommits.loading[sessionId] : false, - ); + const commits = useAppStore((state) => { + if (!sessionId) return undefined; + const envKey = state.environmentIdBySessionId[sessionId] ?? sessionId; + return state.sessionCommits.byEnvironmentId[envKey]; + }); + const loading = useAppStore((state) => { + if (!sessionId) return false; + const envKey = state.environmentIdBySessionId[sessionId] ?? sessionId; + return state.sessionCommits.loading[envKey] ?? false; + }); const setSessionCommits = useAppStore((state) => state.setSessionCommits); const setSessionCommitsLoading = useAppStore((state) => state.setSessionCommitsLoading); const connectionStatus = useAppStore((state) => state.connection.status); diff --git a/apps/web/lib/layout/panel-portal-manager.ts b/apps/web/lib/layout/panel-portal-manager.ts index 99f23592c9..3707beb85a 100644 --- a/apps/web/lib/layout/panel-portal-manager.ts +++ b/apps/web/lib/layout/panel-portal-manager.ts @@ -20,15 +20,16 @@ * - **Global** (`sessionId` is `undefined`) — the portal persists across session * switches. Used for panels that read `activeSessionId` reactively from the * store and automatically show the correct data for whichever session is active. - * Examples: sidebar, chat, changes, files, plan. + * Examples: sidebar, chat, terminal, changes, files, plan. + * Note: terminal, changes, and files use `useEnvironmentSessionId()` to stay + * stable across same-environment session switches. * * - **Session-scoped** (`sessionId` is set) — the portal is bound to the session * that created it and is destroyed via `releaseBySession()` when the user * switches away. Used for panels whose content is intrinsically tied to a * specific session's runtime state (container processes, worktree files, etc.) * and cannot simply re-read a store selector to switch context. - * Examples: terminal, file-editor, browser, vscode, commit-detail, diff-viewer, - * pr-detail. + * Examples: file-editor, browser, vscode, commit-detail, diff-viewer, pr-detail. * * See `SESSION_SCOPED_COMPONENTS` in `dockview-desktop-layout.tsx` for the * authoritative list and per-component rationale. diff --git a/apps/web/lib/state/dockview-session-switch.ts b/apps/web/lib/state/dockview-session-switch.ts index 554b2aaba0..a904ccbea5 100644 --- a/apps/web/lib/state/dockview-session-switch.ts +++ b/apps/web/lib/state/dockview-session-switch.ts @@ -80,7 +80,7 @@ function tryFastSessionSwitch(params: SessionSwitchParams): LayoutGroupIds | nul // Fast path: keep the grid structure, clean up ephemeral panels and any // session chat tabs that belong to a different session (cross-task switch). - // Session-scoped portals (terminal, browser, etc.) will be re-acquired + // Session-scoped portals (browser, vscode, etc.) will be re-acquired // via usePortalSlot's sessionId dependency change. removeEphemeralPanels(api, newSessionId); api.layout(params.safeWidth, params.safeHeight); diff --git a/apps/web/lib/state/slices/session-runtime/migrate-env-keyed-data.test.ts b/apps/web/lib/state/slices/session-runtime/migrate-env-keyed-data.test.ts new file mode 100644 index 0000000000..fb1e5efb07 --- /dev/null +++ b/apps/web/lib/state/slices/session-runtime/migrate-env-keyed-data.test.ts @@ -0,0 +1,91 @@ +import { describe, it, expect } from "vitest"; +import { create } from "zustand"; +import { immer } from "zustand/middleware/immer"; +import { createSessionRuntimeSlice } from "./session-runtime-slice"; +import type { SessionRuntimeSlice } from "./types"; + +function makeStore() { + return create()(immer(createSessionRuntimeSlice)); +} + +describe("registerSessionEnvironment — migrateEnvKeyedData", () => { + it("migrates data from sessionId key to environmentId key", () => { + const store = makeStore(); + + // Simulate data arriving under the fallback sessionId key + store.getState().setSessionCommits("sess-1", [{ commit_sha: "abc" } as never]); + store.getState().setGitStatus("sess-1", { branch: "main" } as never); + store.getState().appendShellOutput("sess-1", "hello"); + + // Register the environment mapping + store.getState().registerSessionEnvironment("sess-1", "env-1"); + + const state = store.getState(); + // Data should now live under the environmentId key + expect(state.sessionCommits.byEnvironmentId["env-1"]).toEqual([{ commit_sha: "abc" }]); + expect(state.gitStatus.byEnvironmentId["env-1"]).toEqual({ branch: "main" }); + expect(state.shell.outputs["env-1"]).toBe("hello"); + // sessionId key should be cleaned up + expect(state.sessionCommits.byEnvironmentId["sess-1"]).toBeUndefined(); + expect(state.gitStatus.byEnvironmentId["sess-1"]).toBeUndefined(); + expect(state.shell.outputs["sess-1"]).toBeUndefined(); + }); + + it("does not clobber pre-existing environmentId data", () => { + const store = makeStore(); + + // Data already stored under the environmentId key + store.getState().registerSessionEnvironment("other-sess", "env-1"); + store.getState().setSessionCommits("other-sess", [{ commit_sha: "existing" } as never]); + + // Stale data under sessionId from before mapping was known + store.getState().setGitStatus("sess-2", { branch: "stale" } as never); + + // A different session wrote commits under its own fallback key + store.setState((draft) => { + draft.sessionCommits.byEnvironmentId["sess-2"] = [{ commit_sha: "orphan" } as never]; + }); + + // Register: sess-2 maps to env-1 (which already has commit data) + store.getState().registerSessionEnvironment("sess-2", "env-1"); + + const state = store.getState(); + // Commits: pre-existing env-1 data preserved, orphaned sess-2 key deleted + expect(state.sessionCommits.byEnvironmentId["env-1"]).toEqual([{ commit_sha: "existing" }]); + expect(state.sessionCommits.byEnvironmentId["sess-2"]).toBeUndefined(); + // Git status: no env-1 data existed, so sess-2 data migrated + expect(state.gitStatus.byEnvironmentId["env-1"]).toEqual({ branch: "stale" }); + expect(state.gitStatus.byEnvironmentId["sess-2"]).toBeUndefined(); + }); + + it("no-ops when sessionId equals environmentId", () => { + const store = makeStore(); + + store.getState().setSessionCommits("local-1", [{ commit_sha: "abc" } as never]); + + store.getState().registerSessionEnvironment("local-1", "local-1"); + + const state = store.getState(); + // Data stays under the same key + expect(state.sessionCommits.byEnvironmentId["local-1"]).toEqual([{ commit_sha: "abc" }]); + }); + + it("migrates only sub-keys that have data", () => { + const store = makeStore(); + + // Only put data in shell outputs and userShells, leave others empty + store.getState().appendShellOutput("sess-3", "output"); + store.getState().setUserShells("sess-3", [{ terminalId: "t1" } as never]); + + store.getState().registerSessionEnvironment("sess-3", "env-3"); + + const state = store.getState(); + expect(state.shell.outputs["env-3"]).toBe("output"); + expect(state.shell.outputs["sess-3"]).toBeUndefined(); + expect(state.userShells.byEnvironmentId["env-3"]).toEqual([{ terminalId: "t1" }]); + expect(state.userShells.byEnvironmentId["sess-3"]).toBeUndefined(); + // Other stores should have no data for either key + expect(state.sessionCommits.byEnvironmentId["env-3"]).toBeUndefined(); + expect(state.sessionCommits.byEnvironmentId["sess-3"]).toBeUndefined(); + }); +}); diff --git a/apps/web/lib/state/slices/session-runtime/session-runtime-slice.ts b/apps/web/lib/state/slices/session-runtime/session-runtime-slice.ts index 0537715e4a..baadeee62c 100644 --- a/apps/web/lib/state/slices/session-runtime/session-runtime-slice.ts +++ b/apps/web/lib/state/slices/session-runtime/session-runtime-slice.ts @@ -78,7 +78,7 @@ export const defaultSessionRuntimeState: SessionRuntimeSliceState = { }, gitStatus: { byEnvironmentId: {} }, environmentIdBySessionId: {}, - sessionCommits: { bySessionId: {}, loading: {} }, + sessionCommits: { byEnvironmentId: {}, loading: {} }, contextWindow: { bySessionId: {} }, agents: { agents: [] }, availableCommands: { bySessionId: {} }, @@ -185,6 +185,35 @@ function buildUserShellActions(set: ImmerSet) { }; } +/** + * Migrate any env-keyed data stored under the fallback `sessionId` key to the + * proper `environmentId` key so selectors don't see stale data after the + * session→environment mapping is registered. + */ +function migrateEnvKeyedData( + draft: SessionRuntimeSliceState, + sessionId: string, + environmentId: string, +) { + if (sessionId === environmentId) return; + const migrate = (store: Record) => { + if (sessionId in store) { + if (!(environmentId in store)) { + store[environmentId] = store[sessionId]; + } + delete store[sessionId]; + } + }; + migrate(draft.sessionCommits.byEnvironmentId); + migrate(draft.sessionCommits.loading); + migrate(draft.gitStatus.byEnvironmentId); + migrate(draft.shell.outputs); + migrate(draft.shell.statuses); + migrate(draft.userShells.byEnvironmentId); + migrate(draft.userShells.loading); + migrate(draft.userShells.loaded); +} + export const createSessionRuntimeSlice: StateCreator< SessionRuntimeSlice, [["zustand/immer", never]], @@ -208,6 +237,7 @@ export const createSessionRuntimeSlice: StateCreator< registerSessionEnvironment: (sessionId, environmentId) => set((draft) => { draft.environmentIdBySessionId[sessionId] = environmentId; + migrateEnvKeyedData(draft, sessionId, environmentId); }), setContextWindow: (sessionId, contextWindow) => set((draft) => { @@ -215,28 +245,32 @@ export const createSessionRuntimeSlice: StateCreator< }), setSessionCommits: (sessionId, commits) => set((draft) => { - draft.sessionCommits.bySessionId[sessionId] = commits; + const envKey = draft.environmentIdBySessionId[sessionId] ?? sessionId; + draft.sessionCommits.byEnvironmentId[envKey] = commits; }), setSessionCommitsLoading: (sessionId, loading) => set((draft) => { - draft.sessionCommits.loading[sessionId] = loading; + const envKey = draft.environmentIdBySessionId[sessionId] ?? sessionId; + draft.sessionCommits.loading[envKey] = loading; }), addSessionCommit: (sessionId, commit) => set((draft) => { - const existing = draft.sessionCommits.bySessionId[sessionId] || []; + const envKey = draft.environmentIdBySessionId[sessionId] ?? sessionId; + const existing = draft.sessionCommits.byEnvironmentId[envKey] || []; // For amend: only replace HEAD (first entry) if it has the same parent if (existing.length > 0 && existing[0].parent_sha === commit.parent_sha) { // Replace HEAD commit (this is an amend) existing[0] = commit; - draft.sessionCommits.bySessionId[sessionId] = existing; + draft.sessionCommits.byEnvironmentId[envKey] = existing; } else { // Normal commit: prepend to list - draft.sessionCommits.bySessionId[sessionId] = [commit, ...existing]; + draft.sessionCommits.byEnvironmentId[envKey] = [commit, ...existing]; } }), clearSessionCommits: (sessionId) => set((draft) => { - delete draft.sessionCommits.bySessionId[sessionId]; + const envKey = draft.environmentIdBySessionId[sessionId] ?? sessionId; + delete draft.sessionCommits.byEnvironmentId[envKey]; }), setAvailableCommands: (sessionId, commands) => set((draft) => { diff --git a/apps/web/lib/state/slices/session-runtime/types.ts b/apps/web/lib/state/slices/session-runtime/types.ts index 49a57c40ff..316c1192d1 100644 --- a/apps/web/lib/state/slices/session-runtime/types.ts +++ b/apps/web/lib/state/slices/session-runtime/types.ts @@ -99,7 +99,7 @@ export type CumulativeDiff = { }; export type SessionCommitsState = { - bySessionId: Record; + byEnvironmentId: Record; loading: Record; }; diff --git a/apps/web/lib/ws/handlers/git-status.ts b/apps/web/lib/ws/handlers/git-status.ts index 7ebfec25ba..d02ef7755e 100644 --- a/apps/web/lib/ws/handlers/git-status.ts +++ b/apps/web/lib/ws/handlers/git-status.ts @@ -18,6 +18,11 @@ type GitEventHandlers = { branch_switched: (store: StoreApi, event: GitBranchSwitchedEvent) => void; }; +/** Resolve sessionId → environmentId for cache keying. */ +function resolveEnvKey(store: StoreApi, sessionId: string): string { + return store.getState().environmentIdBySessionId[sessionId] ?? sessionId; +} + const gitEventHandlers: GitEventHandlers = { status_update: (store, event) => { store.getState().setGitStatus(event.session_id, { @@ -36,7 +41,7 @@ const gitEventHandlers: GitEventHandlers = { branch_deletions: event.status.branch_deletions, }); // Invalidate cumulative diff cache when files change - invalidateCumulativeDiffCache(event.session_id); + invalidateCumulativeDiffCache(resolveEnvKey(store, event.session_id)); }, commit_created: (store, event) => { @@ -55,21 +60,21 @@ const gitEventHandlers: GitEventHandlers = { created_at: event.commit.created_at ?? event.timestamp, }); // Invalidate cumulative diff cache when new commit is created - invalidateCumulativeDiffCache(event.session_id); + invalidateCumulativeDiffCache(resolveEnvKey(store, event.session_id)); }, commits_reset: (store, event) => { // Clear commits to trigger refetch in useSessionCommits hook store.getState().clearSessionCommits(event.session_id); // Invalidate cumulative diff cache when commits are reset - invalidateCumulativeDiffCache(event.session_id); + invalidateCumulativeDiffCache(resolveEnvKey(store, event.session_id)); }, branch_switched: (store, event) => { // Clear commits to trigger refetch with new base commit store.getState().clearSessionCommits(event.session_id); // Invalidate cumulative diff cache when branch switches - invalidateCumulativeDiffCache(event.session_id); + invalidateCumulativeDiffCache(resolveEnvKey(store, event.session_id)); }, };