perf(runtime): index session-tabs tracking keys by environment, and bound the epoch history

Two defects that only appear at scale or over time, in one commit because the
fix for the second calls into the structure the first introduces — see the
coupling note at the end.

1. THE PER-ENVIRONMENT TEARDOWN SWEEP WAS QUADRATIC IN ENVIRONMENT COUNT.
`clearWebSessionTabsTrackingForEnvironment` prefix-scanned every key of eight
module maps, each holding E*W entries, plus one global walk — so a catalog
update clearing E environments cost 8*E^2*W + E*W. It runs on every
pairing-revision change.

Measured with a counting probe on the real function (exact key visits, no
clock, so a loaded machine cannot contaminate it):

  E=10 W=50    500/map    17,000 visits -> 500    (34x)
  E=20 W=50   1000/map    58,740 visits -> 1,000  (59x)
  E=40 W=50   2000/map   202,220 visits -> 2,000  (101x)
  E=50 W=20   1000/map   141,600 visits -> 1,000  (142x)

Doubling E at fixed W: 17,000 -> 58,740 -> 202,220, i.e. 3.45x and 3.44x per
doubling — quadratic, not linear-but-large. The indexed column over the same
sweep is exactly 2x per doubling. Doubling W at fixed E is 1.84x and 1.78x,
linear as expected. These are freshly-populated fixtures with nothing stale in
them, so the quadratic term is a property of the scan itself and not of a leak.
Wall clock for cross-reference only: 1.92ms -> 0.64ms at 20x50, 37.1ms -> 4.8ms
at 100x50.

The fix is the index shape this file already uses for
`hostSessionTabMappingKeysByEnvironmentAndWorktree` — that precedent is why this
is worth indexing rather than tolerating.

The regression assertion is STRUCTURAL, not a timing threshold. A wall-clock
ratio bound was tried first and flaked at 22.3x against a <20x limit under suite
load, which is exactly the failure mode a timing assertion has on this machine.
It now spies on `.keys()` for the nine per-worktree maps and asserts teardown
never enumerates any of them — enumerating them IS the sweep, so it pins the
property directly and is load-independent. Mutation-checked: restoring the
prefix sweep kills it.

2. `sessionTabsPublicationEpochHistoryByWorktree` WAS UNBOUNDED IN KEY COUNT.
Only its inner retired array was capped (at 8); the map itself grew 1:1 with
worktree lifecycles, since the entry is deliberately retained as a tombstone
fence after the live record is dropped. 10,000 create/remove cycles left 10,000
entries. Now LRU-capped at 512, re-inserting on every accepted frame so a
still-publishing worktree is never the eviction victim — the tombstones, which
are what actually accumulate, are evicted first.

WHY ONE COMMIT: the eviction added by (2) calls
`releaseSessionTabsEnvironmentKeyedWorktree`, the index added by (1), because an
evicted tombstone must also release its index entry. Split, the first commit
would leak the index on eviction or the second would not build. The coupling is
real rather than incidental, so they land together.

Verification: 191 files / 1611 tests pass, log grepped for ELIFECYCLE and
failures = 0. pnpm tc and oxlint clean.
This commit is contained in:
Neil
2026-09-10 18:04:03 -07:00
parent cbf18d46d4
commit 20fb0b3f6e
9 changed files with 573 additions and 62 deletions
@@ -0,0 +1,269 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types'
import {
acceptReplayedWebSessionTabsSnapshot,
clearWebSessionTabsTrackingForEnvironment,
decideWebSessionTabsSnapshot,
resetWebSessionTabsSnapshotFreshnessForTests
} from './web-session-tabs-sync'
import {
beginWebSessionTabsSnapshotRecovery,
recordAcceptedWebSessionTabsEnvironment,
recordReceivedWebSessionTabsRemoval,
recordReceivedWebSessionTabsSnapshot,
rememberHostTerminalTabCount,
trackWebSessionTabsWorktree
} from './web-session-tabs-sync/tracking'
import { confirmTrackedWebSessionTabsInventoryAbsence } from './web-session-tabs-sync/session-tabs-inventory-absence'
import {
latestSessionTabsSnapshotByWorktree,
latestReceivedSessionTabsSnapshotByWorktree,
latestSessionTabsRemovalFenceByWorktree,
lastHostTerminalTabCountByWorktree,
replayableSessionTabsSnapshotByWorktree,
sessionTabsEnvironmentsByWorktree,
sessionTabsInventoryOmissionsByWorktree,
sessionTabsPublicationEpochHistoryByWorktree,
sessionTabsRecoveryStateByWorktree,
trackedSessionTabsWorktreeIdsByEnvironment
} from './web-session-tabs-sync/state'
import { _getSessionTabsEnvironmentKeyIndexCountsForTest } from './web-session-tabs-sync/session-tabs-environment-key-index'
vi.mock('../store', () => ({
useAppStore: { setState: vi.fn(), getState: vi.fn(() => ({})) }
}))
vi.mock('./web-session-terminal-handle-events', () => ({
queueAcceptedWebSessionTerminalSnapshot: vi.fn()
}))
const ENVIRONMENTS = 20
const WORKTREES = 50
function envId(index: number): string {
return `env-${String(index).padStart(4, '0')}`
}
function worktreeId(index: number): string {
return `repo::/Users/x/orca/worktrees/workspace-${String(index).padStart(4, '0')}`
}
function snapshotFor(worktree: string, version: number): RuntimeMobileSessionTabsResult {
return {
worktree,
publicationEpoch: `epoch-${version}`,
snapshotVersion: version,
tabs: [{ type: 'terminal', id: 'tab-1' }],
groups: [],
tabBarOrder: [],
activeTabId: null
} as unknown as RuntimeMobileSessionTabsResult
}
/** Populates every per-worktree map through the paths that maintain the key index. */
function populate(environments: number, worktrees: number): void {
resetWebSessionTabsSnapshotFreshnessForTests()
for (let e = 0; e < environments; e += 1) {
const environmentId = envId(e)
for (let w = 0; w < worktrees; w += 1) {
const worktree = worktreeId(w)
const snapshot = snapshotFor(worktree, 1)
const frame = recordReceivedWebSessionTabsSnapshot(environmentId, snapshot)
decideWebSessionTabsSnapshot(snapshot, environmentId)
trackWebSessionTabsWorktree(environmentId, worktree)
rememberHostTerminalTabCount(environmentId, snapshot)
recordAcceptedWebSessionTabsEnvironment(environmentId, snapshot)
beginWebSessionTabsSnapshotRecovery(environmentId, worktree, frame)
confirmTrackedWebSessionTabsInventoryAbsence(environmentId, {
worktree,
freshness: { publicationEpoch: 'epoch-1', snapshotVersion: 1 }
})
}
}
}
/** Every map an environment teardown must reach without enumerating it. */
const SWEPT_MAPS = [
latestSessionTabsSnapshotByWorktree,
replayableSessionTabsSnapshotByWorktree,
latestReceivedSessionTabsSnapshotByWorktree,
sessionTabsPublicationEpochHistoryByWorktree,
latestSessionTabsRemovalFenceByWorktree,
sessionTabsRecoveryStateByWorktree,
lastHostTerminalTabCountByWorktree,
sessionTabsInventoryOmissionsByWorktree,
sessionTabsEnvironmentsByWorktree
]
function keysForEnvironment(environmentId: string): number {
let count = 0
for (const map of [
latestSessionTabsSnapshotByWorktree,
latestReceivedSessionTabsSnapshotByWorktree,
sessionTabsPublicationEpochHistoryByWorktree,
sessionTabsRecoveryStateByWorktree,
lastHostTerminalTabCountByWorktree,
sessionTabsInventoryOmissionsByWorktree
]) {
for (const key of map.keys()) {
if (key.startsWith(`${environmentId}:`)) {
count += 1
}
}
}
return count
}
function environmentsNamingWorktrees(environmentId: string): number {
let count = 0
for (const environments of sessionTabsEnvironmentsByWorktree.values()) {
if (environments.has(environmentId)) {
count += 1
}
}
return count
}
describe('clearWebSessionTabsTrackingForEnvironment at workspace scale', () => {
beforeEach(resetWebSessionTabsSnapshotFreshnessForTests)
it('drains only the torn-down environment across every per-worktree map', () => {
populate(ENVIRONMENTS, WORKTREES)
const victim = envId(0)
const bystander = envId(1)
expect(keysForEnvironment(victim)).toBeGreaterThan(0)
const bystanderKeysBefore = keysForEnvironment(bystander)
clearWebSessionTabsTrackingForEnvironment(victim)
expect(keysForEnvironment(victim)).toBe(0)
expect(environmentsNamingWorktrees(victim)).toBe(0)
expect(trackedSessionTabsWorktreeIdsByEnvironment.has(victim)).toBe(false)
expect(keysForEnvironment(bystander)).toBe(bystanderKeysBefore)
expect(environmentsNamingWorktrees(bystander)).toBe(WORKTREES)
})
it('drains the environment key index that makes teardown linear', () => {
populate(ENVIRONMENTS, WORKTREES)
expect(_getSessionTabsEnvironmentKeyIndexCountsForTest()).toEqual({
environments: ENVIRONMENTS,
worktrees: ENVIRONMENTS * WORKTREES
})
for (let e = 0; e < ENVIRONMENTS; e += 1) {
clearWebSessionTabsTrackingForEnvironment(envId(e))
}
expect(_getSessionTabsEnvironmentKeyIndexCountsForTest()).toEqual({
environments: 0,
worktrees: 0
})
})
it('never enumerates a per-worktree map, so teardown cost tracks one environment', () => {
populate(ENVIRONMENTS, WORKTREES)
// Why: a wall clock cannot separate linear from quadratic on a loaded box.
// Enumerating any of these maps is the sweep itself, so count that instead.
const enumerations = SWEPT_MAPS.map((map) => vi.spyOn(map, 'keys'))
clearWebSessionTabsTrackingForEnvironment(envId(0))
for (const enumeration of enumerations) {
expect(enumeration).not.toHaveBeenCalled()
enumeration.mockRestore()
}
expect(keysForEnvironment(envId(0))).toBe(0)
})
})
/**
* Every write path must register the key index, or teardown silently strands
* that map's entries. Each case populates through exactly one path.
*/
const WRITE_PATHS: readonly {
name: string
write: (environmentId: string, worktree: string) => void
size: () => number
}[] = [
{
name: 'received snapshot',
write: (environmentId, worktree) => {
recordReceivedWebSessionTabsSnapshot(environmentId, snapshotFor(worktree, 1))
},
size: () => latestReceivedSessionTabsSnapshotByWorktree.size
},
{
name: 'publication epoch tombstone',
write: (environmentId, worktree) => {
recordReceivedWebSessionTabsSnapshot(environmentId, snapshotFor(worktree, 1))
},
size: () => sessionTabsPublicationEpochHistoryByWorktree.size
},
{
name: 'accepted snapshot',
write: (environmentId, worktree) => {
decideWebSessionTabsSnapshot(snapshotFor(worktree, 1), environmentId)
},
size: () => latestSessionTabsSnapshotByWorktree.size
},
{
name: 'replayed snapshot',
write: (environmentId, worktree) => {
decideWebSessionTabsSnapshot(snapshotFor(worktree, 1), environmentId)
acceptReplayedWebSessionTabsSnapshot(environmentId, worktree)
},
size: () => replayableSessionTabsSnapshotByWorktree.size
},
{
name: 'pending recovery',
write: (environmentId, worktree) => {
beginWebSessionTabsSnapshotRecovery(environmentId, worktree, 1)
},
size: () => sessionTabsRecoveryStateByWorktree.size
},
{
name: 'removal fence',
write: (environmentId, worktree) => {
beginWebSessionTabsSnapshotRecovery(environmentId, worktree, 1)
recordReceivedWebSessionTabsRemoval(environmentId, worktree, 2)
},
size: () => latestSessionTabsRemovalFenceByWorktree.size
},
{
name: 'host terminal tab count',
write: (environmentId, worktree) => {
rememberHostTerminalTabCount(environmentId, snapshotFor(worktree, 1))
},
size: () => lastHostTerminalTabCountByWorktree.size
},
{
name: 'inventory omission',
write: (environmentId, worktree) => {
confirmTrackedWebSessionTabsInventoryAbsence(environmentId, {
worktree,
freshness: { publicationEpoch: 'epoch-1', snapshotVersion: 1 }
})
},
size: () => sessionTabsInventoryOmissionsByWorktree.size
},
{
name: 'accepted environment',
write: (environmentId, worktree) => {
recordAcceptedWebSessionTabsEnvironment(environmentId, snapshotFor(worktree, 1))
},
size: () => sessionTabsEnvironmentsByWorktree.size
}
]
describe.each(WRITE_PATHS)('teardown after a $name write', ({ write, size }) => {
it('leaves nothing behind', () => {
resetWebSessionTabsSnapshotFreshnessForTests()
for (let w = 0; w < WORKTREES; w += 1) {
write(envId(0), worktreeId(w))
}
expect(size()).toBeGreaterThan(0)
clearWebSessionTabsTrackingForEnvironment(envId(0))
expect(size()).toBe(0)
expect(_getSessionTabsEnvironmentKeyIndexCountsForTest().worktrees).toBe(0)
})
})
@@ -0,0 +1,121 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types'
import {
decideWebSessionTabsSnapshot,
resetWebSessionTabsSnapshotFreshnessForTests
} from './web-session-tabs-sync'
import { clearWebSessionTabsTrackingForWorktree } from './web-session-tabs-sync/tracking-lifecycle'
import { recordReceivedWebSessionTabsSnapshot } from './web-session-tabs-sync/tracking'
import {
MAX_SESSION_TABS_PUBLICATION_EPOCH_HISTORY,
lastHostTerminalTabCountByWorktree,
latestReceivedSessionTabsSnapshotByWorktree,
latestSessionTabsSnapshotByWorktree,
replayableSessionTabsSnapshotByWorktree,
sessionTabsInventoryOmissionsByWorktree,
sessionTabsPublicationEpochHistoryByWorktree,
sessionTabsRuntimeHistoryByEnvironment
} from './web-session-tabs-sync/state'
import { _getSessionTabsEnvironmentKeyIndexCountsForTest } from './web-session-tabs-sync/session-tabs-environment-key-index'
vi.mock('../store', () => ({
useAppStore: { setState: vi.fn(), getState: vi.fn(() => ({})) }
}))
vi.mock('./web-session-terminal-handle-events', () => ({
queueAcceptedWebSessionTerminalSnapshot: vi.fn()
}))
const ENV = 'env-long-running'
const CYCLES = 10_000
function snapshotFor(worktree: string): RuntimeMobileSessionTabsResult {
return {
worktree,
publicationEpoch: `epoch-${worktree}`,
snapshotVersion: 1,
tabs: [],
groups: [],
tabBarOrder: [],
activeTabId: null
} as unknown as RuntimeMobileSessionTabsResult
}
/** Each cycle is one worktree observed then removed by the host, as a long session does for days. */
function driveWorktreeLifecycles(cycles: number): void {
for (let i = 0; i < cycles; i += 1) {
const worktree = `repo::/w/${i}`
recordReceivedWebSessionTabsSnapshot(ENV, snapshotFor(worktree), undefined, `runtime-${i}`)
clearWebSessionTabsTrackingForWorktree(ENV, worktree)
}
}
describe('long-running session growth of session-tabs tracking maps', () => {
beforeEach(resetWebSessionTabsSnapshotFreshnessForTests)
it('bounds the publication epoch tombstone history across 10k worktree lifecycles', () => {
driveWorktreeLifecycles(CYCLES)
expect(sessionTabsPublicationEpochHistoryByWorktree.size).toBe(
MAX_SESSION_TABS_PUBLICATION_EPOCH_HISTORY
)
expect(MAX_SESSION_TABS_PUBLICATION_EPOCH_HISTORY).toBeLessThanOrEqual(1024)
// Why: the tombstone is the only map a worktree removal deliberately retains.
expect(latestSessionTabsSnapshotByWorktree.size).toBe(0)
expect(replayableSessionTabsSnapshotByWorktree.size).toBe(0)
expect(latestReceivedSessionTabsSnapshotByWorktree.size).toBe(0)
expect(lastHostTerminalTabCountByWorktree.size).toBe(0)
expect(sessionTabsInventoryOmissionsByWorktree.size).toBe(0)
})
it('evicts the key index alongside the tombstone it was retained for', () => {
driveWorktreeLifecycles(CYCLES)
expect(_getSessionTabsEnvironmentKeyIndexCountsForTest().worktrees).toBe(
MAX_SESSION_TABS_PUBLICATION_EPOCH_HISTORY
)
})
it('retains only the newest tombstones, so a just-removed worktree stays fenced', () => {
driveWorktreeLifecycles(CYCLES)
expect(sessionTabsPublicationEpochHistoryByWorktree.has(`${ENV}:repo::/w/${CYCLES - 1}`)).toBe(
true
)
expect(sessionTabsPublicationEpochHistoryByWorktree.has(`${ENV}:repo::/w/0`)).toBe(false)
})
it('keeps a still-publishing worktree fenced while removed neighbours churn past the cap', () => {
const live = 'repo::/w/live'
const liveFrame = (version: number): RuntimeMobileSessionTabsResult => ({
...snapshotFor(live),
publicationEpoch: 'epoch-live',
snapshotVersion: version
})
// Fill to the cap with the live worktree noted first, so it is the oldest insert.
decideWebSessionTabsSnapshot(liveFrame(1), ENV)
for (let i = 0; i < MAX_SESSION_TABS_PUBLICATION_EPOCH_HISTORY - 1; i += 1) {
const worktree = `repo::/w/churn-${i}`
recordReceivedWebSessionTabsSnapshot(ENV, snapshotFor(worktree))
clearWebSessionTabsTrackingForWorktree(ENV, worktree)
}
expect(sessionTabsPublicationEpochHistoryByWorktree.size).toBe(
MAX_SESSION_TABS_PUBLICATION_EPOCH_HISTORY
)
// Why: a live frame must move the entry off the eviction front, or the next
// removed worktree evicts a worktree that is still publishing.
decideWebSessionTabsSnapshot(liveFrame(2), ENV)
recordReceivedWebSessionTabsSnapshot(ENV, snapshotFor('repo::/w/churn-overflow'))
expect(sessionTabsPublicationEpochHistoryByWorktree.has(`${ENV}:${live}`)).toBe(true)
expect(sessionTabsPublicationEpochHistoryByWorktree.has(`${ENV}:repo::/w/churn-0`)).toBe(false)
})
it('bounds the retired runtime-id history for one environment', () => {
driveWorktreeLifecycles(CYCLES)
expect(
sessionTabsRuntimeHistoryByEnvironment.get(ENV)?.retired.length ?? 0
).toBeLessThanOrEqual(8)
})
})
@@ -1,5 +1,7 @@
import type { RuntimeRpcResponse } from '../../../../shared/runtime-rpc-envelope'
import { releaseSessionTabsEnvironmentKeyedWorktree } from './session-tabs-environment-key-index'
import {
MAX_SESSION_TABS_PUBLICATION_EPOCH_HISTORY,
latestReceivedSessionTabsFrameByEnvironment,
sessionTabsPublicationEpochHistoryByWorktree,
sessionTabsRuntimeHistoryByEnvironment,
@@ -155,6 +157,8 @@ export function isHeadlessMergeSessionTabsPublication(publicationEpoch: string):
}
export function noteSessionTabsPublicationEpoch(
environmentId: string,
worktreeId: string,
key: string,
publicationEpoch: string
): SessionTabsPublicationEpochHistory {
@@ -162,7 +166,29 @@ export function noteSessionTabsPublicationEpoch(
sessionTabsPublicationEpochHistoryByWorktree.get(key),
publicationEpoch,
SESSION_TABS_RETIRED_EPOCH_LIMIT
)
) as SessionTabsPublicationEpochHistory
history.environmentId = environmentId
history.worktreeId = worktreeId
// Re-insert so map order is least-recently-noted first: every accepted frame
// renotes its epoch, so eviction reaches removed worktrees' tombstones first.
sessionTabsPublicationEpochHistoryByWorktree.delete(key)
sessionTabsPublicationEpochHistoryByWorktree.set(key, history)
evictOldestSessionTabsPublicationEpochHistory()
return history
}
function evictOldestSessionTabsPublicationEpochHistory(): void {
while (
sessionTabsPublicationEpochHistoryByWorktree.size > MAX_SESSION_TABS_PUBLICATION_EPOCH_HISTORY
) {
const oldestKey = sessionTabsPublicationEpochHistoryByWorktree.keys().next().value
if (typeof oldestKey !== 'string') {
return
}
const oldest = sessionTabsPublicationEpochHistoryByWorktree.get(oldestKey)
sessionTabsPublicationEpochHistoryByWorktree.delete(oldestKey)
if (oldest?.environmentId !== undefined && oldest.worktreeId !== undefined) {
releaseSessionTabsEnvironmentKeyedWorktree(oldest.environmentId, oldest.worktreeId, oldestKey)
}
}
}
@@ -0,0 +1,83 @@
import {
lastHostTerminalTabCountByWorktree,
latestReceivedSessionTabsSnapshotByWorktree,
latestSessionTabsRemovalFenceByWorktree,
latestSessionTabsSnapshotByWorktree,
replayableSessionTabsSnapshotByWorktree,
sessionTabsInventoryOmissionsByWorktree,
sessionTabsPublicationEpochHistoryByWorktree,
sessionTabsRecoveryStateByWorktree
} from './state'
/**
* Reverse index from an environment to the worktrees it has minted per-worktree
* keys for. Environment teardown runs once per environment on every pairing
* revision change, so scanning every key of every per-worktree map made that
* cost quadratic in the environment count. Mirrors the shape already used by
* `hostSessionTabMappingKeysByEnvironmentAndWorktree`.
*/
const keyedWorktreeIdsByEnvironment = new Map<string, Set<string>>()
export function noteSessionTabsEnvironmentKeyedWorktree(
environmentId: string,
worktreeId: string
): void {
const worktreeIds = keyedWorktreeIdsByEnvironment.get(environmentId)
if (worktreeIds) {
worktreeIds.add(worktreeId)
return
}
keyedWorktreeIdsByEnvironment.set(environmentId, new Set([worktreeId]))
}
/** Snapshot so callers may delete index entries while iterating. */
export function getSessionTabsEnvironmentKeyedWorktrees(environmentId: string): string[] {
return [...(keyedWorktreeIdsByEnvironment.get(environmentId) ?? [])]
}
/** Drops the index entry once the last per-worktree map has released the key. */
export function releaseSessionTabsEnvironmentKeyedWorktree(
environmentId: string,
worktreeId: string,
key: string
): void {
if (
latestSessionTabsSnapshotByWorktree.has(key) ||
replayableSessionTabsSnapshotByWorktree.has(key) ||
latestReceivedSessionTabsSnapshotByWorktree.has(key) ||
sessionTabsPublicationEpochHistoryByWorktree.has(key) ||
latestSessionTabsRemovalFenceByWorktree.has(key) ||
sessionTabsRecoveryStateByWorktree.has(key) ||
lastHostTerminalTabCountByWorktree.has(key) ||
sessionTabsInventoryOmissionsByWorktree.has(key)
) {
return
}
const worktreeIds = keyedWorktreeIdsByEnvironment.get(environmentId)
if (!worktreeIds) {
return
}
worktreeIds.delete(worktreeId)
if (worktreeIds.size === 0) {
keyedWorktreeIdsByEnvironment.delete(environmentId)
}
}
export function dropSessionTabsEnvironmentKeyIndex(environmentId: string): void {
keyedWorktreeIdsByEnvironment.delete(environmentId)
}
export function clearSessionTabsEnvironmentKeyIndex(): void {
keyedWorktreeIdsByEnvironment.clear()
}
export function _getSessionTabsEnvironmentKeyIndexCountsForTest(): {
environments: number
worktrees: number
} {
let worktrees = 0
for (const worktreeIds of keyedWorktreeIdsByEnvironment.values()) {
worktrees += worktreeIds.size
}
return { environments: keyedWorktreeIdsByEnvironment.size, worktrees }
}
@@ -10,10 +10,7 @@ import {
type TrackedWebSessionTabsWorktree
} from './state'
import { sessionTabsFreshnessKey } from './tracking'
function omissionKey(environmentId: string, worktreeId: string): string {
return `${environmentId}:${worktreeId}`
}
import { noteSessionTabsEnvironmentKeyedWorktree } from './session-tabs-environment-key-index'
function trackedWorktreeOmissionFingerprint(
trackedWorktree: TrackedWebSessionTabsWorktree
@@ -28,7 +25,7 @@ export function clearTrackedWebSessionTabsInventoryAbsence(
environmentId: string,
worktreeId: string
): void {
sessionTabsInventoryOmissionsByWorktree.delete(omissionKey(environmentId, worktreeId))
sessionTabsInventoryOmissionsByWorktree.delete(sessionTabsFreshnessKey(environmentId, worktreeId))
}
/**
@@ -41,11 +38,12 @@ export function confirmTrackedWebSessionTabsInventoryAbsence(
environmentId: string,
trackedWorktree: TrackedWebSessionTabsWorktree
): boolean {
const key = omissionKey(environmentId, trackedWorktree.worktree)
const key = sessionTabsFreshnessKey(environmentId, trackedWorktree.worktree)
const fingerprint = trackedWorktreeOmissionFingerprint(trackedWorktree)
const cached = sessionTabsInventoryOmissionsByWorktree.get(key)
const observations = cached?.fingerprint === fingerprint ? cached.observations + 1 : 1
sessionTabsInventoryOmissionsByWorktree.delete(key)
noteSessionTabsEnvironmentKeyedWorktree(environmentId, trackedWorktree.worktree)
sessionTabsInventoryOmissionsByWorktree.set(key, {
fingerprint,
observations: Math.min(observations, 2)
@@ -26,7 +26,11 @@ export const HOST_WORKING_CLIENT_BOUNDARY_LIMIT = 512
export type SessionTabsStreamEvent =
| (RuntimeMobileSessionTabsResult & { type: 'snapshot' | 'updated' })
| { type: 'snapshots'; snapshots: RuntimeMobileSessionTabsResult[]; authoritative?: boolean }
| {
type: 'snapshots'
snapshots: RuntimeMobileSessionTabsResult[]
authoritative?: boolean
}
| { type: 'end' }
export type SessionTabsListAllResult = {
@@ -62,7 +66,13 @@ export type SessionTabsRuntimeHistory = RetiredValueHistory
* of epochs that have already been superseded so those delayed frames cannot
* roll the mirror back after the replacement epoch is accepted.
*/
export type SessionTabsPublicationEpochHistory = RetiredValueHistory
export type SessionTabsPublicationEpochHistory = RetiredValueHistory & {
/** Set by `noteSessionTabsPublicationEpoch` so eviction can release the key index. */
environmentId?: string
worktreeId?: string
}
/** Live worktrees refresh their entry on every accepted frame, so eviction reaches tombstones first. */
export const MAX_SESSION_TABS_PUBLICATION_EPOCH_HISTORY = 512
export type SessionTabsRecoveryState = { pendingCount: number }
export type SessionTabsRemovalFence = {
receivedFrame: number
@@ -142,8 +152,12 @@ export function resetReceivedSessionTabsFrameSequence(): void {
}
export type TerminalSurface = RuntimeMobileSessionTerminalClientTab
export type ReadyTerminalSurface = RuntimeMobileSessionTerminalClientTab & { status: 'ready' }
export type ReadyBrowserSurface = RuntimeMobileSessionBrowserTab & { browserPageId: string }
export type ReadyTerminalSurface = RuntimeMobileSessionTerminalClientTab & {
status: 'ready'
}
export type ReadyBrowserSurface = RuntimeMobileSessionBrowserTab & {
browserPageId: string
}
export type ReadyEditorSurface = RuntimeMobileSessionMarkdownTab | RuntimeMobileSessionFileTab
export type MirroredAgentTab = { hostTabId: string; unifiedTab: Tab }
@@ -164,7 +178,11 @@ export type MirroredBrowserTab = {
hostTabId: string
clientGroupId?: string
}
export type MirroredEditorTab = { file: OpenFile; unifiedTab: Tab; hostTabId: string }
export type MirroredEditorTab = {
file: OpenFile
unifiedTab: Tab
hostTabId: string
}
export type WebSessionTabsSyncState = Pick<
AppState,
@@ -20,6 +20,7 @@ import {
recordAcceptedWebSessionTabsEnvironment
} from './tracking'
import { clearWebSessionTabsTrackingForWorktree } from './tracking-lifecycle'
import { noteSessionTabsEnvironmentKeyedWorktree } from './session-tabs-environment-key-index'
import { queueAcceptedWebSessionTerminalSnapshot } from '../web-session-terminal-handle-events'
import { shouldAutoCreateInitialTerminal } from '@/components/terminal/initial-terminal'
import { hostSnapshotAffirmsWorktreeContents } from '../host-session-snapshot-authority'
@@ -71,7 +72,13 @@ export function decideWebSessionTabsSnapshot(
// Inventory omissions use a client-only sentinel epoch; recording that
// sentinel would retire the host epoch and reject the next live frame.
if (snapshot.publicationEpoch !== VISIBILITY_INVENTORY_REMOVAL_EPOCH) {
noteSessionTabsPublicationEpoch(key, snapshot.publicationEpoch)
noteSessionTabsEnvironmentKeyedWorktree(environmentId, snapshot.worktree)
noteSessionTabsPublicationEpoch(
environmentId,
snapshot.worktree,
key,
snapshot.publicationEpoch
)
}
clearWebSessionTabsTrackingForWorktree(environmentId, snapshot.worktree)
queueAcceptedWebSessionTerminalSnapshot(snapshot, environmentId)
@@ -119,7 +126,8 @@ export function decideWebSessionTabsSnapshot(
}
rememberHostTerminalTabCount(environmentId, snapshot)
replayableSessionTabsSnapshotByWorktree.delete(key)
noteSessionTabsPublicationEpoch(key, snapshot.publicationEpoch)
noteSessionTabsPublicationEpoch(environmentId, snapshot.worktree, key, snapshot.publicationEpoch)
noteSessionTabsEnvironmentKeyedWorktree(environmentId, snapshot.worktree)
latestSessionTabsSnapshotByWorktree.set(key, {
publicationEpoch: snapshot.publicationEpoch,
snapshotVersion: snapshot.snapshotVersion
@@ -44,6 +44,12 @@ import {
import { clearHostSessionMirrorHydration } from '../host-session-mirror-hydration'
import { clearHostMirrorHandleGapVerdictsForEnvironment } from '@/lib/host-mirror-handle-gap-wait'
import { clearHostSessionTabIdMappings } from './tracking-mappings'
import {
clearSessionTabsEnvironmentKeyIndex,
dropSessionTabsEnvironmentKeyIndex,
getSessionTabsEnvironmentKeyedWorktrees,
noteSessionTabsEnvironmentKeyedWorktree
} from './session-tabs-environment-key-index'
import {
sessionTabsFreshnessKey,
untrackWebSessionTabsWorktree,
@@ -78,6 +84,7 @@ export function acceptReplayedWebSessionTabsSnapshot(
const key = sessionTabsFreshnessKey(environmentId, worktreeId)
const current = latestSessionTabsSnapshotByWorktree.get(key)
if (current) {
noteSessionTabsEnvironmentKeyedWorktree(environmentId, worktreeId)
replayableSessionTabsSnapshotByWorktree.set(key, current)
}
}
@@ -99,6 +106,7 @@ export function resetWebSessionTabsSnapshotFreshnessForTests(): void {
hostSessionTabIdByLocalKey.clear()
hostSessionTabMappingKeysByEnvironmentAndWorktree.clear()
hostWorkingClientBoundaryByPaneKey.clear()
clearSessionTabsEnvironmentKeyIndex()
resetWebSessionBrowserPlacementsForTests()
}
@@ -167,58 +175,29 @@ export function clearWebSessionTabsTrackingForEnvironment(environmentId: string)
if (!trimmedEnvironmentId) {
return
}
const keyPrefix = `${trimmedEnvironmentId}:`
sessionTabsTrackingGenerationByEnvironment.set(
trimmedEnvironmentId,
(sessionTabsTrackingGenerationByEnvironment.get(trimmedEnvironmentId) ?? 0) + 1
)
for (const key of latestSessionTabsSnapshotByWorktree.keys()) {
if (key.startsWith(keyPrefix)) {
latestSessionTabsSnapshotByWorktree.delete(key)
}
}
for (const key of replayableSessionTabsSnapshotByWorktree.keys()) {
if (key.startsWith(keyPrefix)) {
replayableSessionTabsSnapshotByWorktree.delete(key)
}
}
for (const key of latestReceivedSessionTabsSnapshotByWorktree.keys()) {
if (key.startsWith(keyPrefix)) {
latestReceivedSessionTabsSnapshotByWorktree.delete(key)
}
}
sessionTabsRuntimeHistoryByEnvironment.delete(trimmedEnvironmentId)
for (const key of sessionTabsPublicationEpochHistoryByWorktree.keys()) {
if (key.startsWith(keyPrefix)) {
sessionTabsPublicationEpochHistoryByWorktree.delete(key)
}
}
latestReceivedSessionTabsFrameByEnvironment.delete(trimmedEnvironmentId)
latestReceivedSessionTabsInventoryFrameByEnvironment.delete(trimmedEnvironmentId)
for (const key of latestSessionTabsRemovalFenceByWorktree.keys()) {
if (key.startsWith(keyPrefix)) {
latestSessionTabsRemovalFenceByWorktree.delete(key)
}
}
for (const key of sessionTabsRecoveryStateByWorktree.keys()) {
if (key.startsWith(keyPrefix)) {
sessionTabsRecoveryStateByWorktree.delete(key)
}
}
trackedSessionTabsWorktreeIdsByEnvironment.delete(trimmedEnvironmentId)
for (const worktreeId of sessionTabsEnvironmentsByWorktree.keys()) {
// Why: a prefix scan of every per-worktree map costs O(all environments x all
// worktrees) per teardown, and teardown runs once per environment.
for (const worktreeId of getSessionTabsEnvironmentKeyedWorktrees(trimmedEnvironmentId)) {
const key = sessionTabsFreshnessKey(trimmedEnvironmentId, worktreeId)
latestSessionTabsSnapshotByWorktree.delete(key)
replayableSessionTabsSnapshotByWorktree.delete(key)
latestReceivedSessionTabsSnapshotByWorktree.delete(key)
sessionTabsPublicationEpochHistoryByWorktree.delete(key)
latestSessionTabsRemovalFenceByWorktree.delete(key)
sessionTabsRecoveryStateByWorktree.delete(key)
lastHostTerminalTabCountByWorktree.delete(key)
sessionTabsInventoryOmissionsByWorktree.delete(key)
removeWebSessionTabsEnvironment(trimmedEnvironmentId, worktreeId)
}
for (const key of lastHostTerminalTabCountByWorktree.keys()) {
if (key.startsWith(keyPrefix)) {
lastHostTerminalTabCountByWorktree.delete(key)
}
}
for (const key of sessionTabsInventoryOmissionsByWorktree.keys()) {
if (key.startsWith(keyPrefix)) {
sessionTabsInventoryOmissionsByWorktree.delete(key)
}
}
dropSessionTabsEnvironmentKeyIndex(trimmedEnvironmentId)
sessionTabsRuntimeHistoryByEnvironment.delete(trimmedEnvironmentId)
latestReceivedSessionTabsFrameByEnvironment.delete(trimmedEnvironmentId)
latestReceivedSessionTabsInventoryFrameByEnvironment.delete(trimmedEnvironmentId)
trackedSessionTabsWorktreeIdsByEnvironment.delete(trimmedEnvironmentId)
const mappingKeysByWorktree =
hostSessionTabMappingKeysByEnvironmentAndWorktree.get(trimmedEnvironmentId)
if (mappingKeysByWorktree) {
@@ -22,6 +22,7 @@ import {
noteSessionTabsPublicationEpoch,
recordReceivedWebSessionTabsEnvironmentFrame
} from './publisher-identity-fences'
import { noteSessionTabsEnvironmentKeyedWorktree } from './session-tabs-environment-key-index'
export function isSessionTabsListAllResult(value: unknown): value is SessionTabsListAllResult {
return (
@@ -108,7 +109,8 @@ export function recordReceivedWebSessionTabsSnapshot(
return frame
}
if (!history || history.current !== publicationEpoch) {
noteSessionTabsPublicationEpoch(key, publicationEpoch)
noteSessionTabsEnvironmentKeyedWorktree(environmentId, snapshot.worktree)
noteSessionTabsPublicationEpoch(environmentId, snapshot.worktree, key, publicationEpoch)
}
// Stream delivery order is the freshest evidence even when a host's version
// counter briefly moves backwards (for example across a visibility resume).
@@ -121,6 +123,7 @@ export function recordReceivedWebSessionTabsSnapshot(
snapshot.snapshotVersion > current.snapshotVersion ||
(snapshot.snapshotVersion === current.snapshotVersion && current.receivedFrame <= frame)
) {
noteSessionTabsEnvironmentKeyedWorktree(environmentId, snapshot.worktree)
latestReceivedSessionTabsSnapshotByWorktree.set(key, {
receivedFrame: frame,
publicationEpoch,
@@ -147,8 +150,11 @@ export function beginWebSessionTabsSnapshotRecovery(
receivedFrame: number
): () => void {
const key = sessionTabsFreshnessKey(environmentId, worktreeId)
const recoveryState = sessionTabsRecoveryStateByWorktree.get(key) ?? { pendingCount: 0 }
const recoveryState = sessionTabsRecoveryStateByWorktree.get(key) ?? {
pendingCount: 0
}
recoveryState.pendingCount += 1
noteSessionTabsEnvironmentKeyedWorktree(environmentId, worktreeId)
sessionTabsRecoveryStateByWorktree.set(key, recoveryState)
let settled = false
return () => {
@@ -191,6 +197,7 @@ export function recordReceivedWebSessionTabsRemoval(
latestSessionTabsRemovalFenceByWorktree.delete(key)
return
}
noteSessionTabsEnvironmentKeyedWorktree(environmentId, worktreeId)
latestSessionTabsRemovalFenceByWorktree.set(key, {
receivedFrame,
recoveryState,
@@ -244,6 +251,7 @@ export function recordAcceptedWebSessionTabsEnvironment(
environments.delete(environmentId)
}
if (environments.size > 0) {
noteSessionTabsEnvironmentKeyedWorktree(environmentId, snapshot.worktree)
sessionTabsEnvironmentsByWorktree.set(snapshot.worktree, environments)
} else {
sessionTabsEnvironmentsByWorktree.delete(snapshot.worktree)
@@ -266,5 +274,6 @@ export function rememberHostTerminalTabCount(
): void {
const key = sessionTabsFreshnessKey(environmentId, snapshot.worktree)
const terminalCount = snapshot.tabs.filter((tab) => tab.type === 'terminal').length
noteSessionTabsEnvironmentKeyedWorktree(environmentId, snapshot.worktree)
lastHostTerminalTabCountByWorktree.set(key, terminalCount)
}