diff --git a/.agents/rules/README.md b/.agents/rules/README.md index 2493e41..1089382 100644 --- a/.agents/rules/README.md +++ b/.agents/rules/README.md @@ -12,9 +12,12 @@ service. - Do not consume alternate event/history services or Host-private transport contracts. `ctx.agents` owns create/resume/get and returns live Agent handles. -- Agent Trace consumes only `@cordisx/protocol/sessions/v1` and is honestly - unavailable when the Host service, exact route permission, or subscription - is unavailable or replaced. +- Agent Trace consumes `@cordisx/protocol/sessions/v1` plus only the + `EntityDefinitionBoundSessionEvent` extension from + `@cordisx/protocol/entities/v1`. It never queries `ctx.entities` or relabels + Session history from a mutable current registry. It is honestly unavailable + when the Host service, exact route permission, or subscription is unavailable + or replaced. - Every Session capability must be optional and dynamically bound by the Host to the active same-plugin route's exact `:sessionId`; never use an empty or wildcard scope. diff --git a/README.md b/README.md index 03c569a..e0f0287 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,17 @@ The Agent/Session Runtime ownership model is: - environment composition and transport bindings remain Host-private and do not change plugin contracts, permissions, or Session facts. -The contract is `@cordisx/protocol/sessions/v1` at Protocol main commit -`8891722a7735a3bd00bdd5315084b35b748f5e7f`. The Timeline reads a fixed -Session snapshot, pages through its immutable watermark, then follows the -atomic replay/live subscription. Subscription termination clears the view and -reports the terminal code. +The contracts are `@cordisx/protocol/sessions/v1` and the additive +`EntityDefinitionBoundSessionEvent` from `@cordisx/protocol/entities/v1` at +Protocol main commit `c96c290697f9e802a68c6d3bb094fd27d8d00d1e`. +The Timeline reads a fixed Session snapshot, pages through its immutable +watermark, then follows the atomic replay/live subscription. Subscription +termination clears the view and reports the terminal code. + +For an entity-backed Session, `entity/definition-bound` is the durable source +of the exact historical definition identity, digest, and definition bytes. +The Timeline projects that persisted binding and never queries `ctx.entities` +or relabels prior Session history from the latest local entity revision. The plugin consumes no alternate event service, Host-private import, unsafe cast, raw bridge, adapter, or second ledger. @@ -68,6 +74,7 @@ npm run check `npm run check` runs typecheck, build, focused tests, and `npm pack --dry-run`. `cordisx.plugin.json` is the immutable package manifest. The Protocol dependency is pinned to an exact remotely resolvable main commit. -The Host runtime is formally available at CordisX main commit -`d11df2e0d4a2557dc184f4fcec0b7cd8659e289b`. The package remains private until -real-App integration verification is complete. +The entity-backed Host runtime is formally available at CordisX main commit +`ff9cf8b1ba4e4caffa23abbd767dbba0b8884c8a`; Chatroom's entity-backed consumer +is at `dc1d672b93e6b6a3a29961561546957d66955a1a`. The package remains private +until real-App integration verification is complete. diff --git a/README.zh-Hans.md b/README.zh-Hans.md index f08638d..9f9580b 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -20,11 +20,18 @@ Agent/Session Runtime 的 ownership model 如下: Session 事实。 正式契约是 Protocol main commit -`8891722a7735a3bd00bdd5315084b35b748f5e7f` 的 -`@cordisx/protocol/sessions/v1`。Timeline 先读取固定 Session snapshot, +`c96c290697f9e802a68c6d3bb094fd27d8d00d1e` 的 +`@cordisx/protocol/sessions/v1`,以及 +`@cordisx/protocol/entities/v1` 中增量定义的 +`EntityDefinitionBoundSessionEvent`。Timeline 先读取固定 Session snapshot, 分页读取 immutable watermark,再接续 atomic replay/live subscription;订阅 终止时会清空视图并报告 terminal code。 +对 entity-backed Session,`entity/definition-bound` 是历史定义 identity、 +digest 与 definition bytes 的唯一持久事实。Timeline 只投影该 Session 事件中 +持久化的 binding,绝不查询 `ctx.entities`,也不按最新 local entity revision +重标历史。 + 插件不消费替代 event service,也不使用 Host-private import、unsafe cast、 raw bridge、额外 adapter 或第二 ledger。 @@ -54,6 +61,7 @@ npm run check `npm run check` 包含 typecheck、build、focused tests 与 `npm pack --dry-run`。Protocol dependency 精确 pin 到可从远端解析的 main -commit。Host Runtime 已在 CordisX main commit -`d11df2e0d4a2557dc184f4fcec0b7cd8659e289b` 正式提供;真实 App 集成验证闭合前, -package 保持 private。 +commit。entity-backed Host Runtime 已在 CordisX main commit +`ff9cf8b1ba4e4caffa23abbd767dbba0b8884c8a` 正式提供;Chatroom 的 entity-backed +消费已在 main commit `dc1d672b93e6b6a3a29961561546957d66955a1a` +落地。真实 App 集成验证闭合前,package 保持 private。 diff --git a/cordisx.plugin.json b/cordisx.plugin.json index f6b9756..c02b053 100644 --- a/cordisx.plugin.json +++ b/cordisx.plugin.json @@ -20,7 +20,8 @@ "https://raw.githubusercontent.com/cordisx/cordisx-protocol/main/schemas/session-snapshot.v1.schema.json", "https://raw.githubusercontent.com/cordisx/cordisx-protocol/main/schemas/session-subscribe-request.v1.schema.json", "https://raw.githubusercontent.com/cordisx/cordisx-protocol/main/schemas/session-subscription-page.v1.schema.json", - "https://raw.githubusercontent.com/cordisx/cordisx-protocol/main/schemas/session-subscription-close.v1.schema.json" + "https://raw.githubusercontent.com/cordisx/cordisx-protocol/main/schemas/session-subscription-close.v1.schema.json", + "https://raw.githubusercontent.com/cordisx/cordisx-protocol/main/schemas/entity-session-definition-binding.v1.schema.json" ] }, "dependencies": [], diff --git a/docs/MIGRATION-CHECKPOINT.md b/docs/MIGRATION-CHECKPOINT.md index 1f59503..f1e861f 100644 --- a/docs/MIGRATION-CHECKPOINT.md +++ b/docs/MIGRATION-CHECKPOINT.md @@ -50,7 +50,9 @@ This repository currently owns: - Host-neutral Timeline model, filtering, grouping, and read-only body view; - `TraceShowcaseStore`, which is the read-only Timeline state boundary; - `src/session-store.ts`, which consumes only - `@cordisx/protocol/sessions/v1`, performs immutable snapshot paging, atomic + `@cordisx/protocol/sessions/v1` plus the + `EntityDefinitionBoundSessionEvent` extension from + `@cordisx/protocol/entities/v1`, performs immutable snapshot paging, atomic replay/live subscription, and fail-closed generation/terminal fencing; - truthful unavailable state whenever the public Session service or exact route-bound permission is absent; @@ -65,10 +67,12 @@ adapter/ledger, access a raw bridge, or depend on concrete `ctx.agentLoop`. Formal Protocol dependency: - repository: `CordisX/cordisx-protocol`; -- main commit: `8891722a7735a3bd00bdd5315084b35b748f5e7f`; +- main commit: `c96c290697f9e802a68c6d3bb094fd27d8d00d1e`; - pull requests: `https://github.com/cordisx/cordisx-protocol/pull/77` and - `https://github.com/cordisx/cordisx-protocol/pull/78`; -- public entrypoint: `@cordisx/protocol/sessions/v1`; + `https://github.com/cordisx/cordisx-protocol/pull/78`, plus entities/v1 + `https://github.com/cordisx/cordisx-protocol/pull/79`; +- public entrypoints: `@cordisx/protocol/sessions/v1` and + `@cordisx/protocol/entities/v1`; - status: merged to remote main; focused type, conformance, distribution, diff, naming, and formal-main export readback passed. The broad Protocol workflow retains its pre-existing Manager v2 AJV strict-union failure. Mono @@ -90,11 +94,11 @@ must never manufacture a durable cursor or ledger. Formal Host dependency: - repository: `CordisX/cordisx`; -- main commit: `d11df2e0d4a2557dc184f4fcec0b7cd8659e289b`; +- main commit: `ff9cf8b1ba4e4caffa23abbd767dbba0b8884c8a`; - pull requests: `https://github.com/cordisx/cordisx/pull/224` through - `https://github.com/cordisx/cordisx/pull/231`; + `https://github.com/cordisx/cordisx/pull/236`; - status: merged to remote main and pinned to Protocol - `8891722a7735a3bd00bdd5315084b35b748f5e7f`; focused Agent/Session runtime, + `c96c290697f9e802a68c6d3bb094fd27d8d00d1e`; focused Agent/Session runtime, Shell v4 terminal, permission/scope, no-check type, package allowlist, and diff checks passed. The #231 composition focus passed 14/14 with CLI `tsc --noCheck`. Its broad workflow stopped at @@ -124,6 +128,29 @@ excluded from filesystem watch/import graphs so Vite cannot reinterpret `cordisx/react`, `cordisx/ui`, or JSX-runtime virtual sources as absolute file imports; the real transitive source watch graph remains intact. +## ENTITY_DEFINITION_BINDING_READY handoff + +Formal entity-backed consumers: + +- Host main: `ff9cf8b1ba4e4caffa23abbd767dbba0b8884c8a` + (`https://github.com/cordisx/cordisx/pull/236`); +- Chatroom main: `dc1d672b93e6b6a3a29961561546957d66955a1a` + (`https://github.com/cordisx/plugin-chatroom/pull/10`); +- Protocol main: `c96c290697f9e802a68c6d3bb094fd27d8d00d1e`. + +Host persists `entity/definition-bound` as an ignorable SessionEvent when it +creates an entity-backed Session and resumes only from that persisted binding. +Chatroom creates against the exact current Entity identity and resumes an +existing Session with `definitionSource: 'session-persisted'`. + +Agent Trace recognizes that one durable extension during the existing +snapshot/replay/live path. Its projection exposes +`binding.resolution.identity`, `binding.resolution.digest`, and +`binding.resolution.definition`. It does not inject or query `ctx.entities`, +consult Chatroom configuration, or relabel historical runs from a current +registry snapshot. Other unknown required events still fail closed; unrelated +extensions remain skippable only when explicitly marked `ignorable: true`. + ## Deferred owner actions Until real-App integration verification succeeds: diff --git a/package-lock.json b/package-lock.json index 2fc0cae..2ee5675 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "cordisx": "0.1.0-beta.1" }, "devDependencies": { - "@cordisx/protocol": "github:cordisx/cordisx-protocol#8891722a7735a3bd00bdd5315084b35b748f5e7f", + "@cordisx/protocol": "github:cordisx/cordisx-protocol#c96c290697f9e802a68c6d3bb094fd27d8d00d1e", "jsdom": "^26.1.0", "typescript": "^5.9.2", "vitest": "^3.2.4" @@ -48,8 +48,8 @@ }, "node_modules/@cordisx/protocol": { "version": "0.1.0-alpha.0", - "resolved": "git+ssh://git@github.com/cordisx/cordisx-protocol.git#8891722a7735a3bd00bdd5315084b35b748f5e7f", - "integrity": "sha512-y8AYpz29Dgpfo6YXgH7FWUMdApMNH3424PycwHcCIdHtp6AJqL7OyYVJgiWr5Yp4KUdfFbwlFvWAy/dqxeHpKw==", + "resolved": "git+ssh://git@github.com/cordisx/cordisx-protocol.git#c96c290697f9e802a68c6d3bb094fd27d8d00d1e", + "integrity": "sha512-Q9S3YT30akTq29g3cwe4JRvJ5uRJuNEeOk5Sds8OCjNliOKBnyAewHNmNesN2DQL509sqGX8LZVVFMhnay5j4A==", "dev": true, "license": "AGPL-3.0-or-later", "engines": { diff --git a/package.json b/package.json index e710db6..f19136e 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "cordisx": "0.1.0-beta.1" }, "devDependencies": { - "@cordisx/protocol": "github:cordisx/cordisx-protocol#8891722a7735a3bd00bdd5315084b35b748f5e7f", + "@cordisx/protocol": "github:cordisx/cordisx-protocol#c96c290697f9e802a68c6d3bb094fd27d8d00d1e", "jsdom": "^26.1.0", "typescript": "^5.9.2", "vitest": "^3.2.4" diff --git a/src/react-view.tsx b/src/react-view.tsx index d7dad04..503d406 100644 --- a/src/react-view.tsx +++ b/src/react-view.tsx @@ -23,11 +23,14 @@ function options(values: readonly string[], all: string) { function Detail({ event }: { readonly event: TraceEvent | undefined }) { if (event === undefined) return Select an event to inspect its public projection. + const definition = event.definitionResolution const fields: readonly [string, unknown][] = [ ['Event', event.id], ['Sequence', event.seq], ['Recorded', event.recordedAt], ['Type', event.type], ['Semantic', event.semanticType], ['Phase', event.phase], ['Session', event.sessionId], ['Turn', event.turnId], ['Step', event.stepId], ['Item', event.itemId], ['Message', event.messageId], ['Tool call', event.toolCallId], ['Context', event.contextId], + ['Entity', definition?.identity.agentId], ['Entity revision', definition?.identity.revision], + ['Entity digest', definition?.digest], ['Definition name', definition?.definition.name], ['Source', `${event.source.kind}:${event.source.id}`], ] return <>{event.summary}
{fields.filter(([, value]) => value !== undefined).map(([label, value]) =>
{label}
{String(value)}
)}
{event.payload === undefined ? null :
{JSON.stringify(event.payload, null, 2)}
} diff --git a/src/session-store.ts b/src/session-store.ts index 09d19aa..cd9683c 100644 --- a/src/session-store.ts +++ b/src/session-store.ts @@ -10,6 +10,7 @@ import type { SessionSubscriptionPage, UserMessage, } from '@cordisx/protocol/sessions/v1' +import type { EntityDefinitionResolution } from '@cordisx/protocol/entities/v1' import type { TraceEvent, TraceLane, @@ -72,6 +73,7 @@ function lifecyclePhase(event: SessionEvent): TracePhase | undefined { function lane(event: SessionEvent): TraceLane { switch (event.type) { case 'user/message': return 'input' + case 'entity/definition-bound': case 'agent/inbox/spliced': return 'injection' case 'tool/call': case 'tool/result': @@ -109,6 +111,10 @@ function summary(event: SessionEvent): string { case 'agent/inbox/spliced': return `Agent ${event.data.target} inbox changed.` case 'approval/asked': return `Approval requested for ${event.data.toolName}.` case 'approval/decided': return `Approval ${event.data.outcome}.` + case 'entity/definition-bound': { + const { identity, digest, definition } = event.data.resolution + return `Session definition bound to ${definition.name ?? identity.agentId} (${identity.agentId}@${identity.revision}; ${digest}).` + } case 'session/end-seed': return 'Persisted Session seed ended.' default: return unknownEvent(event) } @@ -171,6 +177,9 @@ export function projectSessionEvent(event: SessionEvent): TraceEvent | undefined : event.type === 'tool/result' ? event.data.message.source.callId : undefined + const definitionResolution: EntityDefinitionResolution | undefined = event.type === 'entity/definition-bound' + ? event.data.resolution + : undefined return Object.freeze({ id: `session:${event.sessionId}:${event.seq}`, sessionId: event.sessionId, @@ -186,6 +195,7 @@ export function projectSessionEvent(event: SessionEvent): TraceEvent | undefined ...(step === undefined ? {} : { stepId: step }), ...(messageId === undefined ? {} : { messageId }), ...(toolCallId === undefined ? {} : { toolCallId }), + ...(definitionResolution === undefined ? {} : { definitionResolution }), payload: event.data, }) } diff --git a/src/types.ts b/src/types.ts index 98bdc56..955ee06 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import type { EntityDefinitionResolution } from '@cordisx/protocol/entities/v1' + export type TraceLane = 'input' | 'model' | 'tools' | 'injection' export type TracePhase = | 'opened' | 'resumed' | 'started' | 'updated' | 'completed' @@ -28,6 +30,7 @@ export interface TraceEvent { readonly messageId?: string readonly toolCallId?: string readonly contextId?: string + readonly definitionResolution?: EntityDefinitionResolution readonly payload?: unknown } diff --git a/test/architecture-guard.test.ts b/test/architecture-guard.test.ts index d1680cf..10f1c45 100644 --- a/test/architecture-guard.test.ts +++ b/test/architecture-guard.test.ts @@ -13,6 +13,7 @@ describe('architecture guard', () => { 'ctx.agentEvents', 'ctx.agentHistory', 'ctx.agentLoop', + 'ctx.entities', 'agentEvents', 'agentHistory', 'electronBridge', @@ -23,6 +24,7 @@ describe('architecture guard', () => { 'getElementById', 'packages/cli/src', 'packages/host', + 'EntityRegistry', ]) expect(source).not.toContain(forbidden) }) diff --git a/test/session-store.test.ts b/test/session-store.test.ts index 20430db..1741b93 100644 --- a/test/session-store.test.ts +++ b/test/session-store.test.ts @@ -10,9 +10,11 @@ import type { SessionSubscriptionClosed, SessionSubscriptionPage, } from '@cordisx/protocol/sessions/v1' +import type { EntityDefinitionBoundSessionEvent } from '@cordisx/protocol/entities/v1' import { SessionTraceStore, projectSessionEvent } from '../src/session-store.js' const schema = 'https://raw.githubusercontent.com/cordisx/cordisx-protocol/main/schemas/session-event.v1.schema.json' as const +const persistedDigest = `sha256:${'a'.repeat(64)}` as const function event(seq: number, type: SessionEvent['type'], data: unknown): SessionEvent { return { @@ -27,6 +29,40 @@ function event(seq: number, type: SessionEvent['type'], data: unknown): SessionE } as unknown as SessionEvent } +function entityDefinitionBoundEvent(seq: number): EntityDefinitionBoundSessionEvent { + const identity = { agentId: 'chatroom.generalist', revision: persistedDigest } + return { + $schema: schema, + contract: 'cordisx.session-event/v1', + schemaVersion: 1, + sessionId: 'session-a', + seq, + time: Date.UTC(2026, 0, 1, 0, 0, seq), + type: 'entity/definition-bound', + ignorable: true, + data: { + source: 'entity-registry', + owner: { profileId: 'profile-a', installationId: 'installation-a', pluginId: 'chatroom' }, + resolution: { + identity, + digest: persistedDigest, + definition: { + $schema: 'https://raw.githubusercontent.com/cordisx/cordisx-protocol/main/schemas/agent-definition.v1.schema.json', + contract: 'cordisx.agent-definition/v1', + schemaVersion: 1, + identity, + name: 'Persisted Generalist', + inherit: { + promptSections: 'none', rules: 'none', skills: 'none', + tools: 'none', mcpServers: 'none', runtimeDefaults: 'none', + }, + promptSections: [{ sectionId: 'role', kind: 'role', text: 'Persisted role bytes.' }], + }, + }, + }, + } +} + function page(afterSeq: number, snapshotSeq: number, events: readonly SessionEvent[]): SessionEventPage { const nextAfterSeq = events.at(-1)?.seq ?? afterSeq return { @@ -166,6 +202,33 @@ describe('SessionTraceStore', () => { expect(harness.unsubscribe).toHaveBeenCalledOnce() }) + it('carries the persisted entity binding through the same snapshot, replay, and live cursor path', async () => { + const harness = sessionHarness({ + snapshotSeq: 0, + pages: [page(-1, 0, [entityDefinitionBoundEvent(0)])], + replay: [event(1, 'turn/start', { turn: 1 })], + }) + const store = new SessionTraceStore(harness.sessions, 'session-a', 50) + await store.settled() + + expect(store.getSnapshot().events.map(item => item.type)).toEqual([ + 'entity/definition-bound', + 'turn/start', + ]) + expect(store.getSnapshot().events[0]?.definitionResolution?.definition.name).toBe('Persisted Generalist') + + await harness.observer()!(subscriptionPage('live', 1, [ + event(2, 'user/message', { + id: 'message-user', role: 'user', content: [{ type: 'text', text: 'Continue' }], source: { kind: 'user' }, + }), + ])) + expect(store.getSnapshot()).toMatchObject({ + events: [{ type: 'entity/definition-bound' }, { type: 'turn/start' }, { type: 'user/message' }], + range: { totalAvailable: 3 }, + }) + store.dispose() + }) + it('fails closed and clears rows on a sequence or generation fence violation', async () => { const harness = sessionHarness({ snapshotSeq: 1, @@ -245,6 +308,29 @@ describe('SessionTraceStore', () => { }) describe('SessionEvent projection', () => { + it('attributes entity-backed history only to the Session-persisted definition binding', () => { + const projected = projectSessionEvent(entityDefinitionBoundEvent(0)) + + expect(projected).toMatchObject({ + type: 'entity/definition-bound', + lane: 'injection', + definitionResolution: { + identity: { agentId: 'chatroom.generalist', revision: persistedDigest }, + digest: persistedDigest, + definition: { + name: 'Persisted Generalist', + promptSections: [{ text: 'Persisted role bytes.' }], + }, + }, + payload: { + source: 'entity-registry', + resolution: { identity: { agentId: 'chatroom.generalist', revision: persistedDigest } }, + }, + }) + expect(projected?.summary).toContain(`chatroom.generalist@${persistedDigest}`) + expect(projected?.summary).not.toContain('Latest local revision') + }) + it('projects every durable core variant from the formal v1 vocabulary', () => { const variants: readonly [SessionEvent['type'], unknown][] = [ ['turn/start', { turn: 1 }], diff --git a/test/view.test.tsx b/test/view.test.tsx index e11393f..d34ce76 100644 --- a/test/view.test.tsx +++ b/test/view.test.tsx @@ -5,7 +5,7 @@ import { describe, expect, it, vi } from 'vitest' import type { CordisXReactPageProps } from 'cordisx/contracts' import { UnavailableTraceStore } from '../src/unavailable-store.js' import { createTraceReactPage } from '../src/react-view.js' -import type { TraceShowcaseStore } from '../src/types.js' +import type { TraceEvent, TraceShowcaseStore } from '../src/types.js' vi.mock('cordisx/ui', async () => { const React = await import('react') @@ -86,6 +86,20 @@ function availableStore(): TraceShowcaseStore { } } +function eventStore(event: TraceEvent): TraceShowcaseStore { + const baseline = availableStore().getSnapshot() + const snapshot = Object.freeze({ + ...baseline, + events: Object.freeze([event]), + range: Object.freeze({ loaded: 1, totalAvailable: 1, renderedLimit: 100 }), + }) + return { + getSnapshot: () => snapshot, + subscribe: () => () => {}, + dispose: () => {}, + } +} + describe('Host-owned Agent Trace body', () => { it('renders SessionEvent rows, detail, and no Host chrome', async () => { const dom = new JSDOM('
') @@ -116,6 +130,41 @@ describe('Host-owned Agent Trace body', () => { root.unmount() }) + it('shows the Session-persisted entity identity, digest, and definition', async () => { + const dom = new JSDOM('
') + const digest = `sha256:${'a'.repeat(64)}` as const + const identity = { agentId: 'chatroom.generalist', revision: digest } + const store = eventStore(Object.freeze({ + id: 'session:session-a:0', sessionId: 'session-a', seq: 0, + recordedAt: '2026-01-01T00:00:00.000Z', lane: 'injection', + type: 'entity/definition-bound', semanticType: 'entity/definition-bound', + summary: 'Session definition bound to Persisted Generalist.', + source: Object.freeze({ kind: 'session', id: 'session-a', label: 'Host Session authority' }), + definitionResolution: { + identity, + digest, + definition: { + $schema: 'https://raw.githubusercontent.com/cordisx/cordisx-protocol/main/schemas/agent-definition.v1.schema.json', + contract: 'cordisx.agent-definition/v1', schemaVersion: 1, + identity, name: 'Persisted Generalist', + inherit: { + promptSections: 'none', rules: 'none', skills: 'none', + tools: 'none', mcpServers: 'none', runtimeDefaults: 'none', + }, + }, + }, + })) + const root = await mount(dom, store) + + dom.window.document.querySelector('.cat-row')!.click() + await new Promise(resolve => setTimeout(resolve, 20)) + const detail = dom.window.document.querySelector('.cat-detail')?.textContent + expect(detail).toContain('chatroom.generalist') + expect(detail).toContain(digest) + expect(detail).toContain('Persisted Generalist') + root.unmount() + }) + it('disposes the prior Session store when the Host route replaces sessionId', async () => { const dom = new JSDOM('
') Object.defineProperty(globalThis, 'window', { configurable: true, value: dom.window })