perf(persistence): skip redundant whole-state flushes on terminal reattach (#20137)

* perf(persistence): add pty-binding fast lane to skip redundant flushes

Terminal pane reattachment currently clones the session and serializes the
entire 9.2 MB app state even when the binding is already in place and durable.
Add an early-return fast path that skips this work when all nine predicates
hold: no split, binding matches in-memory and on-disk, incarnation matches,
no tombstone, and generation counter proves durability.

Includes one-line fix in `writeToDiskSync` to record hash-matched sync flushes
as durable, so the fast path doesn't stay parked behind a stale generation.

Adds `persistence.pty-binding` observability spans (local NDJSON, unsampled for
mutations, budgeted for fast-lane hits) to measure eligibility rates before
and after. Includes ratchet test to ensure every binding writer bumps the
generation. Diagnostic tools and full investigation notes from September 7,
2026 capture that identified the 59–100 ms no-op binds and measured a real
terminal keystroke queued 117 ms behind one such call.

* perf(persistence): add pty-binding fast lane to skip redundant flushes

Rapid rebinds of already-durable PTY bindings (e.g., remounting panes)
were unnecessarily expensive because they cloned and flushed the entire
document state every time. Detect when a binding hasn't changed since the
last durable write and skip to return immediately, eliminating main-thread
cost on that path.

* perf(persistence): record binding.origin on the pty-binding span

Fresh spawns always flush, so a fast-lane rate over all calls is diluted
by however many terminals the user opened. Each caller knows whether it
is a spawn, a reattach, a split, or a relay reattach; pass that through
as metadata and record it so the reattach hit rate can be read from the
trace file. Never branched on.

* fix(persistence): keep the tab row on its first pane when a sibling pane binds

A tab row names one PTY, but a split tab holds several panes. The
renderer keeps the row on the first pane and refuses to let later
split-pane spawns steal it, since a remount reattaches the tab to
whatever the row says. Main overwrote it with whichever pane was binding,
and the renderer's next publish put it back, so every sibling reattach
was a state change and could never take the fast lane. On the real
profile that is 38% of panes.

Rewrite the row only when it names nothing useful: null, the PTY this
leaf is replacing, or a PTY no leaf holds. The fast-lane predicate
compares against the same rule.

* perf(persistence): record durable pty-binding flushes per pane

The global write generation is held back by any unrelated dirty
state, causing bindings unchanged for minutes to appear unpersisted
despite being on disk. Track per-pane durability to skip redundant
flushes.

* docs(persistence): describe the per-pane durability record

The durability section still described the global generation check as the
whole story and claimed there was no binding durability cache. Record the
measurement that motivated the per-pane record, and why retiring one needs
no cooperation from other binding writers.

* docs(perf): consolidate every measured Orca performance issue into one register

Folds the findings from all related debug sessions into the live lag
investigation: the persistence/main-thread work (P1-P11), host contention
(H1-H5), git and subprocess load on main (G1-G8), renderer and terminal
rendering (R1-R8), the terminal daemon session leak from the deleted
debug-orca-perf-issue worktree (D1-D9), and the Cmd-J palette review (C1-C6).

Keeps the measurement behind each claim, records what is fixed versus open,
and restates what the 117 ms keystroke delay still does not explain.

* fix: address performance review findings

* fix: satisfy diagnostic probe lint

* chore: keep investigation artifacts out of performance PR

* fix: run lag probe regression tests with Vitest

* perf(persistence): replace pane receipts with global durability check

* refactor(persistence): remove redundant binding review machinery

* test(persistence): satisfy current assertion-free quality gate

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
This commit is contained in:
Jinjing
2026-09-14 01:04:43 -04:00
committed by GitHub
co-authored by Jinwoo-H
parent 7d98c8e2f3
commit d2d32691ef
27 changed files with 1940 additions and 226 deletions
+184
View File
@@ -0,0 +1,184 @@
import { mkdtemp, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import {
startRendererTimingProbe,
stopRendererTimingProbe
} from './idle-cpu-renderer-timing-probe.mjs'
// Called with an already-attached main Orca page; never launches, focuses, or reloads it.
export async function captureLiveInputLag(page, durationMs = 30_000) {
if (!Number.isFinite(durationMs) || durationMs < 1_000 || durationMs > 60_000) {
throw new Error('Capture duration must be 160 seconds')
}
const identity = await page.evaluate(async () => {
if (!window.api?.app?.getIdentity) {
throw new Error('Target is not the main Orca renderer')
}
if (window.__orcaLiveInputLag || window.__orcaIdleCpuTimingProbe) {
throw new Error('A renderer timing probe already exists; stop it before capturing')
}
return window.api.app.getIdentity()
})
const directory = await mkdtemp(join(tmpdir(), 'orca-input-lag-'))
const cdp = await page.context().newCDPSession(page)
let timingStarted = false
let inputStarted = false
let profilingStarted = false
try {
await startRendererTimingProbe(page)
timingStarted = true
await page.evaluate(() => {
const events = []
const frames = []
const observers = []
const maxEntries = 3_000
let dropped = 0
const retain = (list, value) => {
if (list.length < maxEntries) {
list.push(value)
} else {
dropped++
}
}
const surface = (target) => {
if (!(target instanceof Element)) {
return 'other'
}
if (target.closest('.xterm')) {
return 'terminal'
}
if (target.closest('.monaco-editor')) {
return 'editor'
}
if (target.closest('[contenteditable="true"]')) {
return 'contenteditable'
}
return target.matches('input, textarea') ? 'text-input' : 'other'
}
const onInput = (event) => {
retain(events, {
kind: 'listener',
type: event.type,
surface: surface(event.target),
eventAt: event.timeStamp,
handlerAt: performance.now(),
trusted: event.isTrusted
})
}
const types = ['keydown', 'beforeinput', 'input', 'compositionstart', 'compositionend']
for (const type of types) {
document.addEventListener(type, onInput, true)
}
const supported = PerformanceObserver.supportedEntryTypes ?? []
if (supported.includes('event')) {
const observer = new PerformanceObserver((list) => {
for (const event of list.getEntries()) {
if (!types.includes(event.name)) {
continue
}
retain(events, {
kind: 'event-timing',
type: event.name,
surface: surface(event.target),
eventAt: event.startTime,
processingStart: event.processingStart,
processingEnd: event.processingEnd,
duration: event.duration,
interactionId: event.interactionId
})
}
})
observer.observe({ type: 'event', durationThreshold: 16 })
observers.push(observer)
}
let last = performance.now()
let frameId
const frame = (now) => {
if (now - last > 32) {
retain(frames, { at: now, gapMs: now - last })
}
last = now
frameId = requestAnimationFrame(frame)
}
frameId = requestAnimationFrame(frame)
const startedAt = performance.now()
const startedAtIso = new Date().toISOString()
window.__orcaLiveInputLag = {
stop: () => {
cancelAnimationFrame(frameId)
for (const type of types) {
document.removeEventListener(type, onInput, true)
}
for (const observer of observers) {
observer.disconnect()
}
delete window.__orcaLiveInputLag
return {
startedAt,
startedAtIso,
endedAt: performance.now(),
visibility: document.visibilityState,
events,
frames,
dropped,
eventTimingSupported: supported.includes('event')
}
}
}
})
inputStarted = true
await cdp.send('Profiler.enable')
const profileStartWindow = [await page.evaluate(() => performance.now())]
await cdp.send('Profiler.start')
profilingStarted = true
profileStartWindow.push(await page.evaluate(() => performance.now()))
await new Promise((resolve) => setTimeout(resolve, durationMs))
const { profile } = await cdp.send('Profiler.stop')
profilingStarted = false
const input = await page.evaluate(() => window.__orcaLiveInputLag.stop())
inputStarted = false
const timing = await stopRendererTimingProbe(page)
await page.evaluate(() => {
delete window.__orcaIdleCpuTimingProbe
})
timingStarted = false
await writeFile(join(directory, 'renderer.cpuprofile'), JSON.stringify(profile), {
mode: 0o600
})
await writeFile(
join(directory, 'input-timing.json'),
JSON.stringify(
{
identity,
input,
timing,
profileStartWindow,
limitation:
'Keyboard dispatch, handlers and frames only; not PTY echo latency. Event Timing omits short events and rounds durations.'
},
null,
2
),
{ mode: 0o600 }
)
return { directory, eventRecords: input.events.length, slowFrames: input.frames.length, timing }
} finally {
if (profilingStarted) {
await cdp.send('Profiler.stop').catch(() => {})
}
if (inputStarted) {
await page.evaluate(() => window.__orcaLiveInputLag?.stop()).catch(() => {})
}
if (timingStarted) {
await stopRendererTimingProbe(page).catch(() => {})
await page
.evaluate(() => {
delete window.__orcaIdleCpuTimingProbe
})
.catch(() => {})
}
await cdp.send('Profiler.disable').catch(() => {})
await cdp.detach().catch(() => {})
}
}
@@ -0,0 +1,72 @@
import { writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { captureLiveInputLag } from './capture-live-input-lag.mjs'
import { connectOrcaMainInspector } from './orca-main-inspector-connection.mjs'
// Diagnostic-only adapter: Electron CDP on the verified existing renderer; no window actions.
const expectedPid = Number(process.argv[2])
const rendererId = Number(process.argv[3])
if (!expectedPid || !rendererId) {
throw new Error('Usage: node capture-running-orca-lag.mjs MAIN_PID WEB_CONTENTS_ID')
}
const connection = await connectOrcaMainInspector(expectedPid, rendererId)
const { send, evaluateMain, evaluateRenderer, contents } = connection
let attached = false
let echoStarted = false
let mainProfiling = false
try {
const identity = await evaluateMain(
`({pid:process.pid,type:${contents}.getType(),rendererPid:${contents}.getOSProcessId(),attached:${contents}.debugger.isAttached()})`
)
if (identity.pid !== expectedPid || identity.type !== 'window' || identity.attached) {
throw new Error(`Unexpected or already-debugged target: ${JSON.stringify(identity)}`)
}
const before = await evaluateRenderer('window.__orcaTypingDiagnostic.report()')
if (before.sampling.running) {
throw new Error('An existing typing diagnostic is running')
}
await evaluateMain(`${contents}.debugger.attach('1.3')`)
attached = true
const page = {
evaluate: (fn, arg) => evaluateRenderer(`(${fn.toString()})(${JSON.stringify(arg) ?? ''})`),
context: () => ({
newCDPSession: async () => ({
send: connection.cdp,
detach: async () => {}
})
})
}
await evaluateRenderer('window.__orcaTypingDiagnostic.start()')
echoStarted = true
await send('Profiler.enable')
await send('Profiler.start')
mainProfiling = true
console.log(
JSON.stringify({ captureStarted: new Date().toISOString(), identity, census: before.census })
)
const result = await captureLiveInputLag(page, 30_000)
const { profile } = await send('Profiler.stop')
mainProfiling = false
await evaluateRenderer('window.__orcaTypingDiagnostic.stop()')
echoStarted = false
const echo = await evaluateRenderer('window.__orcaTypingDiagnostic.report()')
await writeFile(join(result.directory, 'main.cpuprofile'), JSON.stringify(profile), {
mode: 0o600
})
await writeFile(join(result.directory, 'terminal-echo.json'), JSON.stringify(echo, null, 2), {
mode: 0o600
})
console.log(JSON.stringify({ ...result, echo }, null, 2))
} finally {
if (echoStarted) {
await evaluateRenderer('window.__orcaTypingDiagnostic.stop()').catch(() => {})
}
if (mainProfiling) {
await send('Profiler.stop').catch(() => {})
}
await send('Profiler.disable').catch(() => {})
if (attached) {
await evaluateMain(`${contents}.debugger.detach()`).catch(() => {})
}
connection.close()
}
@@ -0,0 +1,82 @@
import assert from 'node:assert/strict'
import { test } from 'vitest'
import { runInNewContext } from 'node:vm'
import { installRendererIpcProbe } from './main-blocking-probe.mjs'
import { connectOrcaMainInspector } from './orca-main-inspector-connection.mjs'
test('IPC polling records a rejection and permits the next poll', async () => {
let poll
let calls = 0
const window = {
api: {
app: {
getIdentity: async () => {
if (++calls === 1) {
throw new Error('IPC disconnected')
}
}
}
}
}
runInNewContext(`(${String(installRendererIpcProbe)})()`, {
window,
performance,
Date,
document: { addEventListener() {}, removeEventListener() {} },
setInterval(callback) {
poll = callback
return 1
},
clearInterval() {}
})
await poll()
await poll()
const { requests } = window.__orcaIpcTimingProbe.stop()
assert.equal(requests.length, 2)
assert.match(requests[0].failed, /IPC disconnected/)
assert.equal(requests[1].failed, undefined)
})
test('socket closure rejects outstanding and subsequent requests without timeout timers', async () => {
let socket
const timers = new Set()
class FakeSocket {
static OPEN = 1
readyState = 1
constructor() {
socket = this
queueMicrotask(() => this.onopen())
}
send(payload) {
const { id, params } = JSON.parse(payload)
if (params.expression === 'process.pid') {
queueMicrotask(() =>
this.onmessage({ data: JSON.stringify({ id, result: { result: { value: 42 } } }) })
)
}
}
close() {
this.readyState = 3
this.onclose()
}
}
const connect = runInNewContext(`(${String(connectOrcaMainInspector)})`, {
fetch: async () => ({ json: async () => [{ webSocketDebuggerUrl: 'ws://fixture' }] }),
WebSocket: FakeSocket,
setTimeout(callback) {
timers.add(callback)
return callback
},
clearTimeout(timer) {
timers.delete(timer)
}
})
const connection = await connect(42)
const first = connection.send('Profiler.start')
const second = connection.send('Profiler.stop')
socket.close()
await assert.rejects(first, /Inspector socket closed/)
await assert.rejects(second, /Inspector socket closed/)
await assert.rejects(connection.send('Profiler.enable'), /not open/)
assert.equal(timers.size, 0)
})
+120
View File
@@ -0,0 +1,120 @@
export function installMainBlockingProbe() {
if (globalThis.__orcaMainBlockingProbe) {
throw new Error('Main blocking probe already exists')
}
const events = []
const cleanup = []
const startedAt = Date.now()
function wrap(object, name, label, sizeOf) {
const original = object[name]
const wrapped = function (...args) {
const start = performance.now()
const epoch = Date.now()
let result
try {
result = Reflect.apply(original, this, args)
return result
} finally {
const durationMs = performance.now() - start
if (durationMs >= 8 && events.length < 2000) {
events.push({
epoch,
durationMs,
label,
size: sizeOf?.(args, result) ?? null,
stack: new Error('Main blocking call').stack?.split('\n').slice(2, 10)
})
}
}
}
object[name] = wrapped
cleanup.push(() => {
if (object[name] === wrapped) {
object[name] = original
}
})
}
wrap(JSON, 'stringify', 'JSON.stringify', (_args, result) => result?.length)
wrap(globalThis, 'structuredClone', 'structuredClone')
wrap(Buffer, 'from', 'Buffer.from', (args) => args[0]?.length)
const hashPrototype = Object.getPrototypeOf(
process.getBuiltinModule('crypto').createHash('sha256')
)
wrap(hashPrototype, 'update', 'hash.update', (args) => args[0]?.length)
const fs = process.getBuiltinModule('fs')
for (const name of ['existsSync', 'accessSync', 'writeFileSync', 'fsyncSync', 'renameSync']) {
wrap(fs, name, name)
}
const timerGaps = []
let previous = performance.now()
const timer = setInterval(() => {
const now = performance.now()
const gap = now - previous - 25
previous = now
if (gap > 20 && timerGaps.length < 2000) {
timerGaps.push({ epoch: Date.now(), gapMs: gap })
}
}, 25)
timer.unref()
globalThis.__orcaMainBlockingProbe = {
stop() {
clearInterval(timer)
for (const restore of cleanup.toReversed()) {
restore()
}
delete globalThis.__orcaMainBlockingProbe
return { startedAt, endedAt: Date.now(), events, timerGaps }
}
}
return { startedAt }
}
export function installRendererIpcProbe() {
if (window.__orcaIpcTimingProbe) {
throw new Error('Renderer IPC probe already exists')
}
const requests = []
const keys = []
let pending = false
let stopped = false
const timer = setInterval(async () => {
if (pending || stopped) {
return
}
pending = true
const start = performance.now()
const epoch = Date.now()
try {
await window.api.app.getIdentity()
if (requests.length < 2000) {
requests.push({ epoch, durationMs: performance.now() - start })
}
} catch (error) {
if (requests.length < 2000) {
requests.push({ epoch, durationMs: performance.now() - start, failed: String(error) })
}
} finally {
pending = false
}
}, 100)
const keydown = (event) => {
if (keys.length < 1000) {
keys.push({
epoch: Date.now(),
queueMs: performance.now() - event.timeStamp,
terminal: !!event.target?.closest?.('.xterm'),
trusted: event.isTrusted
})
}
}
document.addEventListener('keydown', keydown, true)
window.__orcaIpcTimingProbe = {
stop() {
stopped = true
clearInterval(timer)
document.removeEventListener('keydown', keydown, true)
delete window.__orcaIpcTimingProbe
return { requests, keys }
}
}
}
@@ -0,0 +1,84 @@
export async function connectOrcaMainInspector(expectedPid, rendererId = 1) {
const [target] = await (await fetch('http://127.0.0.1:9229/json/list')).json()
const socket = new WebSocket(target.webSocketDebuggerUrl)
await new Promise((resolve, reject) => {
socket.onopen = resolve
socket.onerror = reject
})
const pending = new Map()
let nextId = 0
socket.onclose = () => {
for (const [id, callback] of pending) {
pending.delete(id)
clearTimeout(callback.timer)
callback.reject(new Error('Inspector socket closed'))
}
}
socket.onmessage = (event) => {
const message = JSON.parse(event.data)
const callback = pending.get(message.id)
if (!callback) {
return
}
pending.delete(message.id)
clearTimeout(callback.timer)
if (message.error) {
callback.reject(new Error(JSON.stringify(message.error)))
} else {
callback.resolve(message.result)
}
}
function send(method, params = {}) {
return new Promise((resolve, reject) => {
if (socket.readyState !== WebSocket.OPEN) {
reject(new Error('Inspector socket is not open'))
return
}
const id = ++nextId
const timer = setTimeout(() => {
pending.delete(id)
reject(new Error(`Timed out: ${method}`))
}, 15_000)
pending.set(id, { resolve, reject, timer })
try {
socket.send(JSON.stringify({ id, method, params }))
} catch (error) {
pending.delete(id)
clearTimeout(timer)
reject(error)
}
})
}
async function evaluateMain(expression) {
const result = await send('Runtime.evaluate', {
expression,
returnByValue: true,
awaitPromise: true
})
if (result.exceptionDetails) {
throw new Error(result.result.description ?? JSON.stringify(result.exceptionDetails))
}
return result.result.value
}
try {
if ((await evaluateMain('process.pid')) !== expectedPid) {
throw new Error('Inspector belongs to a different main process')
}
} catch (error) {
socket.close()
throw error
}
const contents = `process.getBuiltinModule('module').createRequire(process.execPath)('electron').webContents.fromId(${rendererId})`
return {
send,
evaluateMain,
contents,
evaluateRenderer: (expression) =>
evaluateMain(`${contents}.executeJavaScript(${JSON.stringify(expression)})`),
cdp: (method, params = {}) =>
evaluateMain(
`${contents}.debugger.sendCommand(${JSON.stringify(method)},${JSON.stringify(params)})`
),
close: () => socket.close()
}
}
+85
View File
@@ -0,0 +1,85 @@
export function installPersistenceCallProbe() {
const store = globalThis.__orcaLiveStoreProbeTarget
if (!store || globalThis.__orcaPersistenceCallProbe) {
throw new Error('Missing verified live store, or probe already active')
}
const contextSymbol = Object.getOwnPropertySymbols(store).find(
(symbol) => symbol.description === 'PrimaryStateWriteOperations'
)
const serialization = store[contextSymbol]?.serialization
if (!serialization?.buildStateToSave) {
throw new Error('Live serialization context was not found')
}
const events = []
const cleanup = []
function wrap(object, name, describe) {
const descriptor = Object.getOwnPropertyDescriptor(object, name)
const original = object[name]
const wrapped = function (...args) {
const details = describe?.(args) ?? {}
const start = performance.now()
const epoch = Date.now()
let result
try {
result = Reflect.apply(original, this, args)
return result
} finally {
const durationMs = performance.now() - start
if (events.length < 1000) {
events.push({
name,
epoch,
durationMs,
...details,
payloadBytes: name === 'buildStateToSave' ? result?.payload?.length : undefined,
stack:
durationMs > 20
? new Error('Persistence timing').stack?.split('\n').slice(2, 9)
: undefined
})
}
}
}
Object.defineProperty(object, name, { value: wrapped, configurable: true, writable: true })
cleanup.push(() => {
if (object[name] !== wrapped) {
return
}
if (descriptor) {
Object.defineProperty(object, name, descriptor)
} else {
delete object[name]
}
})
}
wrap(serialization, 'buildStateToSave')
wrap(store, 'flushOrThrow')
wrap(store, 'persistPtyBinding', ([args, hostId]) => {
if (hostId && hostId !== 'local') {
return { local: false }
}
const session = store.getWorkspaceSession()
const key = `${args.tabId}:${args.leafId}`
const worktreeId = args.expectedSourceBinding?.worktreeId ?? args.worktreeId
const tab = session.tabsByWorktree?.[worktreeId]?.find((t) => t.id === args.tabId)
return {
local: true,
tabAlreadyBound: tab?.ptyId === args.ptyId,
leafAlreadyBound:
session.terminalLayoutsByTabId?.[args.tabId]?.ptyIdsByLeafId?.[args.leafId] === args.ptyId,
incarnationAlreadyMatches:
session.terminalPtyIncarnationsByPaneKey?.[key] === args.incarnationId,
layoutExists: !!session.terminalLayoutsByTabId?.[args.tabId]?.root
}
})
globalThis.__orcaPersistenceCallProbe = {
stop() {
for (const restore of cleanup.toReversed()) {
restore()
}
delete globalThis.__orcaPersistenceCallProbe
delete globalThis.__orcaLiveStoreProbeTarget
return { events }
}
}
}
@@ -0,0 +1,129 @@
/* eslint-disable no-control-regex -- Terminal control-sequence metadata, never input text. */
export function installTerminalRenderPhaseProbe() {
if (window.__orcaRenderPhaseProbe || !window.__orcaLiveRenderPanes) {
throw new Error('Missing verified pane references, or another probe is active')
}
const events = []
const cleanup = []
let dropped = 0
const startedAt = performance.now()
function record(pane, kind, extra = {}) {
if (!pane.terminal.element?.contains(document.activeElement)) {
return
}
const core = pane.terminal._core
if (events.length >= 5000) {
dropped++
return
}
events.push({
at: performance.now(),
leafId: pane.leafId,
kind,
paused: core?._renderService?._isPaused,
sync: core?.coreService?.decPrivateModes?.synchronizedOutput,
...extra
})
}
function wrap(object, key, makeWrapper) {
const original = object?.[key]
if (typeof original !== 'function') {
return
}
const wrapped = makeWrapper(original)
object[key] = wrapped
cleanup.push(() => {
if (object[key] === wrapped) {
object[key] = original
}
})
}
function subscribe(object, key, listener) {
const disposable = object?.[key]?.(listener)
if (disposable) {
cleanup.push(() => disposable.dispose())
}
}
for (const pane of window.__orcaLiveRenderPanes) {
const terminal = pane.terminal
const service = terminal?._core?._renderService
if (!service) {
continue
}
subscribe(terminal, 'onData', (data) => record(pane, 'dispatch', { bytes: data.length }))
subscribe(terminal, 'onWriteParsed', () => record(pane, 'parsed'))
subscribe(terminal, 'onRender', () => record(pane, 'public-render'))
subscribe(service, 'onRender', () => record(pane, 'service-render'))
wrap(
terminal,
'write',
(original) =>
function (data, ...args) {
if (terminal.element?.contains(document.activeElement)) {
const text = typeof data === 'string' ? data : new TextDecoder().decode(data)
const controls = [...text.matchAll(/\x1b\[([0-?]*)([ -/]*)([@-~])/g)]
const withoutControls = text
.replace(/\x1b\][\s\S]*?(?:\x07|\x1b\\)/g, '')
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '')
.replace(/\x1b[()][0-~]/g, '')
.replace(/[\x00-\x1f\x7f]/g, '')
record(pane, 'write', {
bytes: data.length,
printableChars: withoutControls.length,
syncStarts: controls.filter((c) => c[1] === '?2026' && c[3] === 'h').length,
syncEnds: controls.filter((c) => c[1] === '?2026' && c[3] === 'l').length,
csiFinals: controls.map((c) => c[3]).join('')
})
}
return original.call(this, data, ...args)
}
)
wrap(
service,
'refreshRows',
(original) =>
function (start, end, sync, redrawOnly) {
record(pane, 'refresh-request', {
start,
end,
synchronous: !!sync,
redrawOnly: !!redrawOnly
})
return original.call(this, start, end, sync, redrawOnly)
}
)
const renderer = service._renderer?.value ?? service._renderer
wrap(
renderer,
'renderRows',
(original) =>
function (...args) {
const before = performance.now()
const result = original.apply(this, args)
record(pane, 'render-rows', { duration: performance.now() - before })
return result
}
)
}
const keydown = (event) => {
const pane = window.__orcaLiveRenderPanes.find((p) =>
p.terminal.element?.contains(event.target)
)
if (pane) {
record(pane, 'keydown', { eventAt: event.timeStamp, trusted: event.isTrusted })
}
}
document.addEventListener('keydown', keydown, true)
cleanup.push(() => document.removeEventListener('keydown', keydown, true))
window.__orcaRenderPhaseProbe = {
stop() {
for (const dispose of cleanup.toReversed()) {
dispose()
}
delete window.__orcaRenderPhaseProbe
delete window.__orcaLiveRenderPanes
return { startedAt, endedAt: performance.now(), events, dropped }
}
}
return { startedAt, panes: window.__orcaLiveRenderPanes.length }
}
@@ -435,7 +435,8 @@ describe('registerPtyHandlers', () => {
worktreeId: 'wt-1',
tabId: 'tab-1',
leafId,
ptyId: 'ssh-pty'
ptyId: 'ssh-pty',
origin: 'spawn'
},
'ssh:ssh-1'
)
@@ -159,7 +159,8 @@ describe('registerPtyHandlers', () => {
leafId,
ptyId: expect.any(String),
incarnationId: expect.any(String),
hostAdmittedMembership: true
hostAdmittedMembership: true,
origin: 'spawn'
})
})
it('shuts down a split PTY when its expected source binding was retired', async () => {
@@ -518,7 +519,8 @@ describe('registerPtyHandlers', () => {
leafId,
ptyId: 'pty-shared',
startupCwd: '/tmp',
hostAdmittedMembership: true
hostAdmittedMembership: true,
origin: 'spawn'
})
})
})
@@ -387,7 +387,8 @@ describe('registerPtyHandlers', () => {
tabId: 'tab-race',
leafId,
ptyId: 'pty-renderer',
startupCwd: '/tmp'
startupCwd: '/tmp',
origin: 'spawn'
})
})
it.each([
@@ -541,7 +541,8 @@ describe('registerPtyHandlers', () => {
tabId: 'tab-remote',
leafId,
ptyId: 'ssh:ssh-1@@relay-pty',
hostAdmittedMembership: true
hostAdmittedMembership: true,
origin: 'spawn'
},
'ssh:ssh-1'
)
@@ -202,7 +202,8 @@ describe('registerPtyHandlers', () => {
tabId: 'tab-remote',
leafId,
ptyId: 'ssh:ssh-reattach-ok@@relay-pty',
hostAdmittedMembership: true
hostAdmittedMembership: true,
origin: 'reattach'
},
'ssh:ssh-reattach-ok'
)
+3 -1
View File
@@ -13,6 +13,7 @@ import { ptyOwnership, ptyIncarnationById, deletePtyOwnership } from '../provide
import { ptySizes } from '../delivery/visibility-state'
import { resolveCommittedPtySize, type PtyGrid } from '../delivery/attached-pty-size'
import { clearProviderPtyState } from '../provider/state-cleanup'
import { spawnCommitBindingOrigin } from '../../../persistence/loading-store/pty-binding-span'
import type { PtyIpcSpawnState } from './spawn-state'
export async function persistPtyIpcSpawnCommit(ctx: PtyIpcSpawnState): Promise<{
@@ -115,7 +116,8 @@ export async function persistPtyIpcSpawnCommit(ctx: PtyIpcSpawnState): Promise<{
leafId: ctx.validatedLeafId,
ptyId: ctx.result.id,
...(ctx.result.incarnationId ? { incarnationId: ctx.result.incarnationId } : {}),
...(ctx.cwd ? { startupCwd: ctx.cwd } : {})
...(ctx.cwd ? { startupCwd: ctx.cwd } : {}),
origin: spawnCommitBindingOrigin(ctx.result)
}
if (args.connectionId) {
ctx.deps.store.persistPtyBinding(binding, toSshExecutionHostId(args.connectionId))
+3 -1
View File
@@ -14,6 +14,7 @@ import {
import { ptyIncarnationById, ptyOwnership } from '../provider/ownership-state'
import { isHostReportedPtyAbsenceError, isObservedPtyExitEvidence } from '../provider/liveness'
import { clearProviderPtyState } from '../provider/state-cleanup'
import { spawnCommitBindingOrigin } from '../../../persistence/loading-store/pty-binding-span'
export type StablePaneOwner = {
handle?: string
@@ -200,7 +201,8 @@ export function persistAdmittedStablePaneBinding(args: {
ptyId: args.result.id,
...(args.result.incarnationId ? { incarnationId: args.result.incarnationId } : {}),
...(args.startupCwd ? { startupCwd: args.startupCwd } : {}),
expectedBinding
expectedBinding,
origin: spawnCommitBindingOrigin(args.result)
},
args.connectionId ? toSshExecutionHostId(args.connectionId) : undefined
)
+3 -1
View File
@@ -34,6 +34,7 @@ import { createTerminalSessionStateSaveFailureMessage } from '../../../../shared
import { clearProviderPtyState } from '../provider/state-cleanup'
import { resolvePaneSpawnReservation } from '../pane/spawn-reservation'
import { admitProviderReattachLaunchIdentity } from '../pane/launch-authority'
import { spawnCommitBindingOrigin } from '../../../persistence/loading-store/pty-binding-span'
import type { RuntimePtySpawnState } from './spawn-state'
export async function commitRuntimePtySpawn(ctx: RuntimePtySpawnState) {
@@ -159,7 +160,8 @@ export async function commitRuntimePtySpawn(ctx: RuntimePtySpawnState) {
...(ctx.cwd ? { startupCwd: ctx.cwd } : {}),
...(ctx.hostSessionBinding.expectedSourceBinding
? { expectedSourceBinding: ctx.hostSessionBinding.expectedSourceBinding }
: {})
: {}),
origin: spawnCommitBindingOrigin(ctx.result, ctx.hostSessionBinding.expectedSourceBinding)
}
const persisted = args.connectionId
? ctx.hostSessionBinding.store.persistPtyBinding(
@@ -11,9 +11,14 @@ import {
dataFile,
writeDataFile,
readDataFile,
makeRepo
makeRepo,
makeTerminalTab
} from './persistence-test-harness'
import { TEST_LEAF_1 } from './persistence-session-fixtures'
import { TEST_LEAF_1, TEST_LEAF_2 } from './persistence-session-fixtures'
import { getDefaultPersistedState, getDefaultWorkspaceSession } from '../shared/constants'
import type { WorkspaceSessionState } from '../shared/workspace-session-state-types'
import { _resetTracerForTests, setActiveSink } from './observability/tracer'
import { _resetPtyBindingSpanSamplingForTests } from './persistence/loading-store/pty-binding-span'
// Stub the ~/.ssh/config parser so the SSH-import test drives the real Store with deterministic hosts, not the operator's actual ~/.ssh/config.
const { loadUserSshConfigMock, sshConfigHostsToTargetsMock } = vi.hoisted(() => ({
@@ -64,6 +69,8 @@ describe('Store', () => {
})
afterEach(() => {
vi.restoreAllMocks()
_resetPtyBindingSpanSamplingForTests()
rmSync(testState.dir, { recursive: true, force: true })
})
// ── 10. flush writes synchronously ─────────────────────────────────
@@ -379,4 +386,367 @@ describe('Store', () => {
store.flush()
expect((readDataFile() as { githubCache?: unknown }).githubCache).toBeUndefined()
})
// ── persistPtyBinding fast lane ────────────────────────────────────
describe('persistPtyBinding fast lane', () => {
const WORKTREE = 'repo1::/worktree'
const binding = { worktreeId: WORKTREE, tabId: 'tab1', leafId: TEST_LEAF_1, ptyId: 'pty-1' }
const paneKey = `tab1:${TEST_LEAF_1}`
const boundSession = (
overrides: Partial<WorkspaceSessionState> = {}
): WorkspaceSessionState => ({
...getDefaultWorkspaceSession(),
activeRepoId: 'repo1',
activeWorktreeId: WORKTREE,
activeTabId: 'tab1',
tabsByWorktree: {
[WORKTREE]: [makeTerminalTab({ id: 'tab1', worktreeId: WORKTREE, ptyId: 'pty-1' })]
},
terminalLayoutsByTabId: {
tab1: {
root: { type: 'leaf', leafId: TEST_LEAF_1 },
activeLeafId: TEST_LEAF_1,
expandedLeafId: null,
ptyIdsByLeafId: { [TEST_LEAF_1]: 'pty-1' }
}
},
...overrides
})
const runtimeCounters = (store: ReturnType<typeof createStore>) => {
const runtime = store['runtime']
return {
writeGeneration: runtime.writeGeneration,
lastDurableWriteGeneration: runtime.lastDurableWriteGeneration
}
}
afterEach(() => {
_resetTracerForTests()
})
it.each([undefined, 'ssh:ssh-1', 'runtime:runtime-1'])(
'skips the clone and the flush when the binding is already durable on %s',
async (hostId) => {
const store = await createStore()
store.setWorkspaceSession(boundSession(), hostId)
expect(store.persistPtyBinding(binding, hostId)).toBe(true)
const inoBefore = statSync(dataFile()).ino
const flushSpy = vi.spyOn(store, 'flushOrThrow')
const cloneSpy = vi.spyOn(globalThis, 'structuredClone')
expect(store.persistPtyBinding(binding, hostId)).toBe(true)
expect(flushSpy).not.toHaveBeenCalled()
expect(cloneSpy).not.toHaveBeenCalled()
expect(statSync(dataFile()).ino).toBe(inoBefore)
}
)
it('flushes while a save is pending, and the sync hash match makes the next call durable', async () => {
const store = await createStore()
store.setWorkspaceSession(boundSession())
store.persistPtyBinding(binding)
const inoBefore = statSync(dataFile()).ino
// Bumps the write generation without changing any binding.
store.setWorkspaceSession({ ...store.getWorkspaceSession() })
const flushSpy = vi.spyOn(store, 'flushOrThrow')
expect(store.persistPtyBinding(binding)).toBe(true)
expect(flushSpy).toHaveBeenCalledTimes(1)
expect(statSync(dataFile()).ino).toBe(inoBefore)
// Without the writeToDiskSync counter fix the hash-match flush leaves the durable
// generation one behind and this third bind would flush again.
expect(store.persistPtyBinding(binding)).toBe(true)
expect(flushSpy).toHaveBeenCalledTimes(1)
})
it('falls through on an incarnation change and persists the new incarnation', async () => {
const store = await createStore()
store.setWorkspaceSession(boundSession())
store.persistPtyBinding({ ...binding, incarnationId: 'a' })
const flushSpy = vi.spyOn(store, 'flushOrThrow')
expect(store.persistPtyBinding({ ...binding, incarnationId: 'b' })).toBe(true)
expect(flushSpy).toHaveBeenCalledTimes(1)
expect(readDataFile()).toHaveProperty(
['workspaceSession', 'terminalPtyIncarnationsByPaneKey', paneKey],
'b'
)
})
it('does not acknowledge an unpersisted binding published after the final flush', async () => {
const store = await createStore()
store.setWorkspaceSession(boundSession())
store.persistPtyBinding(binding)
await store.flushAsync()
const next = boundSession()
next.tabsByWorktree[WORKTREE][0].ptyId = 'pty-after-quit'
next.terminalLayoutsByTabId.tab1.ptyIdsByLeafId = { [TEST_LEAF_1]: 'pty-after-quit' }
store.setWorkspaceSession(next)
expect(store.getWorkspaceSession().tabsByWorktree[WORKTREE][0].ptyId).toBe('pty-after-quit')
expect(() => store.persistPtyBinding({ ...binding, ptyId: 'pty-after-quit' })).toThrow(
'Cannot synchronously flush after final persistence has started'
)
expect(readDataFile()).toHaveProperty(
['workspaceSession', 'tabsByWorktree', WORKTREE, '0', 'ptyId'],
'pty-1'
)
})
it('treats an undefined incarnation against a recorded one as a miss', async () => {
const store = await createStore()
store.setWorkspaceSession(boundSession())
store.persistPtyBinding({ ...binding, incarnationId: 'a' })
const flushSpy = vi.spyOn(store, 'flushOrThrow')
expect(store.persistPtyBinding(binding)).toBe(true)
expect(flushSpy).toHaveBeenCalledTimes(1)
})
it('falls through on a tombstone and lets the write path clear it', async () => {
const persisted = getDefaultPersistedState(testState.dir)
persisted.repos = [makeRepo({ id: 'repo1', path: '/repo1' })]
persisted.workspaceSession = boundSession({
terminalPtyIncarnationsByPaneKey: { [paneKey]: 'inc-1' },
terminalSurfaceTombstonesByPaneKey: {
[paneKey]: {
worktreeId: WORKTREE,
parentTabId: 'tab1',
leafId: TEST_LEAF_1,
ptyId: 'pty-1',
incarnationId: 'inc-1',
retiredAt: 1
}
}
})
writeDataFile(persisted)
const store = await createStore()
expect(
store.getWorkspaceSession().terminalSurfaceTombstonesByPaneKey?.[paneKey]
).toBeDefined()
const flushSpy = vi.spyOn(store, 'flushOrThrow')
expect(store.persistPtyBinding({ ...binding, incarnationId: 'inc-1' })).toBe(true)
expect(flushSpy).toHaveBeenCalledTimes(1)
expect(
store.getWorkspaceSession().terminalSurfaceTombstonesByPaneKey?.[paneKey]
).toBeUndefined()
})
it('still bumps the topology fence for a reconciled incarnation', async () => {
const store = await createStore()
store.setWorkspaceSession(
boundSession({ terminalPtyIncarnationsByPaneKey: { [paneKey]: 'inc-stale' } })
)
store.persistPtyBinding({ ...binding, incarnationId: 'inc-stale' })
const revisionBefore =
store.getWorkspaceSession().terminalTopologyRevisionByRepoId?.repo1 ?? 0
const flushSpy = vi.spyOn(store, 'flushOrThrow')
expect(
store.persistPtyBinding({
...binding,
incarnationId: 'inc-live',
expectedBinding: { ptyId: 'pty-1', incarnationId: 'inc-stale' }
})
).toBe(true)
expect(flushSpy).toHaveBeenCalledTimes(1)
expect(store.getWorkspaceSession().terminalTopologyRevisionByRepoId?.repo1).toBe(
revisionBefore + 1
)
})
it('keeps every refusal ahead of the fast lane', async () => {
const store = await createStore()
store.setWorkspaceSession(
boundSession({ terminalPtyIncarnationsByPaneKey: { [paneKey]: 'inc-1' } })
)
store.persistPtyBinding({ ...binding, incarnationId: 'inc-1' })
const flushSpy = vi.spyOn(store, 'flushOrThrow')
const refusals = [
{
...binding,
expectedSourceBinding: { tabId: 'other-tab', leafId: TEST_LEAF_1, ptyId: 'pty-1' }
},
{ ...binding, incarnationId: 'inc-1', expectedBinding: { ptyId: 'pty-other' } },
{ ...binding, tabId: 'missing-tab', mayCreate: false }
]
for (const refusal of refusals) {
expect(store.persistPtyBinding(refusal)).toBe(false)
}
expect(flushSpy).not.toHaveBeenCalled()
})
it.each([undefined, 'ssh:ssh-1', 'runtime:runtime-1'])(
'flushes unrelated dirty state once, then skips unchanged reattachments on %s',
async (hostId) => {
const store = await createStore()
store.setWorkspaceSession(boundSession(), hostId)
expect(store.persistPtyBinding(binding, hostId)).toBe(true)
store.addRepo(makeRepo({ id: 'r-dirty', path: '/dirty' }))
const flushSpy = vi.spyOn(store, 'flushOrThrow')
expect(store.persistPtyBinding(binding, hostId)).toBe(true)
expect(store.persistPtyBinding(binding, hostId)).toBe(true)
expect(flushSpy).toHaveBeenCalledTimes(1)
expect(readDataFile()).toMatchObject({
repos: expect.arrayContaining([expect.objectContaining({ id: 'r-dirty' })])
})
}
)
it('flushes again once the session object is replaced', async () => {
const store = await createStore()
store.setWorkspaceSession(boundSession())
store.persistPtyBinding(binding)
// A renderer publish schedules another save, so global durability must be re-established.
store.setWorkspaceSession({ ...store.getWorkspaceSession() })
store.addRepo(makeRepo({ id: 'r-dirty', path: '/dirty' }))
const flushSpy = vi.spyOn(store, 'flushOrThrow')
expect(store.persistPtyBinding(binding)).toBe(true)
expect(flushSpy).toHaveBeenCalledTimes(1)
})
it('flushes a changed pty for a pane whose old binding was durable', async () => {
const store = await createStore()
store.setWorkspaceSession(boundSession())
store.persistPtyBinding(binding)
store.addRepo(makeRepo({ id: 'r-dirty', path: '/dirty' }))
const flushSpy = vi.spyOn(store, 'flushOrThrow')
expect(store.persistPtyBinding({ ...binding, ptyId: 'pty-next' })).toBe(true)
expect(flushSpy).toHaveBeenCalledTimes(1)
expect(readDataFile()).toHaveProperty(
['workspaceSession', 'terminalLayoutsByTabId', 'tab1', 'ptyIdsByLeafId', TEST_LEAF_1],
'pty-next'
)
})
it('lets every pane of a split tab hit the fast lane', async () => {
const store = await createStore()
store.setWorkspaceSession(
boundSession({
terminalLayoutsByTabId: {
tab1: {
root: {
type: 'split',
direction: 'vertical',
first: { type: 'leaf', leafId: TEST_LEAF_1 },
second: { type: 'leaf', leafId: TEST_LEAF_2 }
},
activeLeafId: TEST_LEAF_2,
expandedLeafId: null,
ptyIdsByLeafId: { [TEST_LEAF_1]: 'pty-1', [TEST_LEAF_2]: 'pty-2' }
}
}
})
)
const sibling = { ...binding, leafId: TEST_LEAF_2, ptyId: 'pty-2' }
// First remount after a cold park: both panes reattach back to back.
expect(store.persistPtyBinding(binding)).toBe(true)
expect(store.persistPtyBinding(sibling)).toBe(true)
expect(store.getWorkspaceSession().tabsByWorktree?.[WORKTREE]?.[0]?.ptyId).toBe('pty-1')
const flushSpy = vi.spyOn(store, 'flushOrThrow')
// Second remount: neither pane may rewrite the tab row, so neither flushes.
expect(store.persistPtyBinding(sibling)).toBe(true)
expect(store.persistPtyBinding(binding)).toBe(true)
expect(flushSpy).not.toHaveBeenCalled()
expect(store.getWorkspaceSession().tabsByWorktree?.[WORKTREE]?.[0]?.ptyId).toBe('pty-1')
})
it('resolves the SSH partition without re-pointing it', async () => {
const store = await createStore()
const hostId = 'ssh:ssh-1'
store.setWorkspaceSession(boundSession(), hostId)
expect(store.persistPtyBinding(binding, hostId)).toBe(true)
const partitionBefore = store.getWorkspaceSession(hostId)
const partitionsBefore = store['runtime'].state.workspaceSessionsByHostId
const flushSpy = vi.spyOn(store, 'flushOrThrow')
expect(store.persistPtyBinding(binding, hostId)).toBe(true)
expect(flushSpy).not.toHaveBeenCalled()
expect(store.getWorkspaceSession(hostId)).toBe(partitionBefore)
expect(store['runtime'].state.workspaceSessionsByHostId).toBe(partitionsBefore)
expect(store.getWorkspaceSession().tabsByWorktree?.[WORKTREE]).toBeUndefined()
})
it('records a sync hash match as durable', async () => {
const store = await createStore()
store.addRepo(makeRepo())
store.flushOrThrow()
const inoBefore = statSync(dataFile()).ino
const after = runtimeCounters(store)
expect(after.lastDurableWriteGeneration).toBe(after.writeGeneration)
store.flushOrThrow()
expect(statSync(dataFile()).ino).toBe(inoBefore)
const counters = runtimeCounters(store)
expect(counters.writeGeneration).toBe(after.writeGeneration + 1)
expect(counters.lastDurableWriteGeneration).toBe(counters.writeGeneration)
})
it('emits one persistence.pty-binding span per call with its outcome', async () => {
const records: unknown[] = []
setActiveSink({
push: (record) => {
records.push(record)
},
flush: () => {},
close: () => {}
})
const store = await createStore()
store.setWorkspaceSession(boundSession())
store.persistPtyBinding(binding)
store.persistPtyBinding(binding)
store.persistPtyBinding({ ...binding, tabId: 'missing-tab', mayCreate: false })
const spans = records.filter(
(record) =>
typeof record === 'object' &&
record !== null &&
'name' in record &&
record.name === 'persistence.pty-binding'
)
expect(spans).toMatchObject([
{
attributes: {
'binding.outcome': 'flushed',
'binding.eligible': false,
'binding.misses': 'not_durable'
}
},
{
attributes: {
'binding.outcome': 'fast_lane',
'binding.eligible': true,
'binding.generation_gap': 0,
'binding.host': 'local'
}
},
{ attributes: { 'binding.outcome': 'refused' } }
])
expect(JSON.stringify(spans)).not.toContain(TEST_LEAF_1)
expect(JSON.stringify(spans)).not.toContain('pty-1')
})
})
})
@@ -231,6 +231,12 @@ export function writeToDiskSync(
!opts.force &&
stateHash === owner[primaryStateWriteOperationsContext].runtime.lastWrittenStateHash
) {
// Why: flushOrThrow already bumped writeGeneration; the file holds this state, so record it
// durable or persistPtyBinding's fast lane stays parked one generation behind forever.
owner[primaryStateWriteOperationsContext].runtime.lastDurableWriteGeneration = Math.max(
owner[primaryStateWriteOperationsContext].runtime.lastDurableWriteGeneration,
owner[primaryStateWriteOperationsContext].runtime.writeGeneration
)
return
}
const dataFile = owner[primaryStateWriteOperationsContext].runtime.dataFile
@@ -0,0 +1,130 @@
import { describe, expect, it } from 'vitest'
import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
import { getDefaultWorkspaceSession } from '../../../shared/constants'
import { evaluatePtyBindingFastLane } from './pty-binding-fast-lane'
const LEAF = '11111111-1111-4111-8111-111111111111'
const WORKTREE = 'repo1::/worktree'
const request = { tabId: 'tab1', leafId: LEAF, ptyId: 'pty-1' }
const paneKey = `tab1:${LEAF}`
function session(overrides: Partial<WorkspaceSessionState> = {}): WorkspaceSessionState {
return {
...getDefaultWorkspaceSession(),
tabsByWorktree: {
[WORKTREE]: [
{
id: 'tab1',
worktreeId: WORKTREE,
title: 'Terminal',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1,
ptyId: 'pty-1'
}
]
},
terminalLayoutsByTabId: {
tab1: {
root: { type: 'leaf', leafId: LEAF },
activeLeafId: LEAF,
expandedLeafId: null,
ptyIdsByLeafId: { [LEAF]: 'pty-1' }
}
},
...overrides
}
}
describe('evaluatePtyBindingFastLane', () => {
it('is eligible only when memory matches and the session is durable', () => {
expect(evaluatePtyBindingFastLane(request, session(), WORKTREE, true)).toEqual({
eligible: true,
misses: []
})
expect(evaluatePtyBindingFastLane(request, session(), WORKTREE, false)).toEqual({
eligible: false,
misses: ['not_durable']
})
})
it('names every miss', () => {
const miss = (
args: Partial<Parameters<typeof evaluatePtyBindingFastLane>[0]>,
state: WorkspaceSessionState = session()
) => evaluatePtyBindingFastLane({ ...request, ...args }, state, WORKTREE, true).misses
expect(miss({ expectedSourceBinding: {} })).toEqual(['split'])
expect(miss({ leafId: 'legacy-pane-1' })).toEqual(['legacy_leaf', 'leaf_absent', 'leaf_pty'])
expect(miss({}, session({ tabsByWorktree: {} }))).toEqual(['tab_missing'])
expect(miss({ ptyId: 'pty-2' })).toEqual(['tab_pty', 'leaf_pty'])
expect(miss({}, session({ terminalLayoutsByTabId: {} }))).toEqual(['layout_missing'])
expect(
miss(
{},
session({
terminalLayoutsByTabId: {
tab1: { root: null, activeLeafId: null, expandedLeafId: null, ptyIdsByLeafId: {} }
}
})
)
).toEqual(['layout_missing'])
expect(miss({ incarnationId: 'a' })).toEqual(['incarnation'])
expect(miss({}, session({ terminalPtyIncarnationsByPaneKey: { [paneKey]: 'a' } }))).toEqual([
'incarnation'
])
expect(
miss(
{ incarnationId: 'a' },
session({
terminalPtyIncarnationsByPaneKey: { [paneKey]: 'a' },
terminalSurfaceTombstonesByPaneKey: {
[paneKey]: {
worktreeId: WORKTREE,
parentTabId: 'tab1',
leafId: LEAF,
ptyId: 'pty-1',
incarnationId: 'a',
retiredAt: 1
}
}
})
)
).toEqual(['tombstone'])
})
it('accepts a sibling pane whose tab row names the first pane', () => {
const LEAF_B = '22222222-2222-4222-8222-222222222222'
const state = session({
terminalLayoutsByTabId: {
tab1: {
root: {
type: 'split',
direction: 'vertical',
first: { type: 'leaf', leafId: LEAF },
second: { type: 'leaf', leafId: LEAF_B }
},
activeLeafId: LEAF_B,
expandedLeafId: null,
ptyIdsByLeafId: { [LEAF]: 'pty-1', [LEAF_B]: 'pty-2' }
}
}
})
expect(
evaluatePtyBindingFastLane(
{ ...request, leafId: LEAF_B, ptyId: 'pty-2' },
state,
WORKTREE,
true
)
).toEqual({ eligible: true, misses: [] })
})
it('accepts a matching incarnation', () => {
const state = session({ terminalPtyIncarnationsByPaneKey: { [paneKey]: 'a' } })
expect(
evaluatePtyBindingFastLane({ ...request, incarnationId: 'a' }, state, WORKTREE, true).eligible
).toBe(true)
})
})
@@ -0,0 +1,87 @@
import { isTerminalLeafId } from '../../../shared/stable-pane-id'
import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
import { layoutContainsLeafId } from '../restoring-sessions/terminal-layout-normalization'
import { tabRowPtyIdAfterLeafBinding } from './terminal-tab-pty-ownership'
/**
* Why a reattach can be ineligible. `not_durable` alone means memory already matched but the
* binding was still waiting in the debounced save — the bucket that says whether skipping the
* flush on a durable match is enough, or the autosave itself has to move off the main thread.
*/
export type PtyBindingFastLaneMiss =
| 'split'
| 'legacy_leaf'
| 'tab_missing'
| 'tab_pty'
| 'layout_missing'
| 'leaf_absent'
| 'leaf_pty'
| 'incarnation'
| 'tombstone'
| 'not_durable'
export type PtyBindingFastLaneRequest = {
tabId: string
leafId: string
ptyId: string
incarnationId?: string
expectedSourceBinding?: unknown
}
export type PtyBindingFastLaneVerdict = {
eligible: boolean
misses: PtyBindingFastLaneMiss[]
}
/**
* True only when `persistPtyBinding` would change nothing: the requested binding is already the
* in-memory session's binding and that session is already on disk. Every miss falls through to
* the write path, so the predicate must be at least as strict as the mutations it stands in for.
*/
export function evaluatePtyBindingFastLane(
args: PtyBindingFastLaneRequest,
session: WorkspaceSessionState,
bindingWorktreeId: string,
durable: boolean
): PtyBindingFastLaneVerdict {
const misses: PtyBindingFastLaneMiss[] = []
const paneKey = `${args.tabId}:${args.leafId}`
if (args.expectedSourceBinding !== undefined) {
misses.push('split')
}
if (!isTerminalLeafId(args.leafId)) {
misses.push('legacy_leaf')
}
const tab = session.tabsByWorktree?.[bindingWorktreeId]?.find(
(candidate) => candidate.id === args.tabId
)
const layout = session.terminalLayoutsByTabId?.[args.tabId]
if (!tab) {
misses.push('tab_missing')
} else if (
tab.ptyId !== tabRowPtyIdAfterLeafBinding(tab, layout?.ptyIdsByLeafId, args.leafId, args.ptyId)
) {
misses.push('tab_pty')
}
if (!layout || !layout.root) {
misses.push('layout_missing')
} else {
if (!layoutContainsLeafId(layout.root, args.leafId)) {
misses.push('leaf_absent')
}
if (layout.ptyIdsByLeafId?.[args.leafId] !== args.ptyId) {
misses.push('leaf_pty')
}
}
// Strict: undefined on both sides matches, undefined on one side does not.
if (session.terminalPtyIncarnationsByPaneKey?.[paneKey] !== args.incarnationId) {
misses.push('incarnation')
}
if (session.terminalSurfaceTombstonesByPaneKey?.[paneKey]) {
misses.push('tombstone')
}
if (!durable) {
misses.push('not_durable')
}
return { eligible: misses.length === 0, misses }
}
@@ -1,5 +1,6 @@
import { LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host'
import { LOCAL_EXECUTION_HOST_ID, parseExecutionHostId } from '../../../shared/execution-host'
import { isTerminalLeafId } from '../../../shared/stable-pane-id'
import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id'
import {
cloneLayoutNode,
@@ -15,8 +16,45 @@ import type { PtyBindingSourceExpectation } from './store'
import type { StoreRuntimeState } from './store-runtime-state'
import type { SessionHostPartitionOperations } from './session-host-partitions'
import { resolveHostId } from './session-host-partitions'
import { evaluatePtyBindingFastLane } from './pty-binding-fast-lane'
import { ptyBindingIsRefused } from './pty-binding-refusals'
import { startPtyBindingSpan, type PtyBindingOrigin } from './pty-binding-span'
import { tabRowPtyIdAfterLeafBinding } from './terminal-tab-pty-ownership'
type PtyBindingPersistenceOperationsRuntime = Pick<StoreRuntimeState, 'flushOrThrow' | 'state'>
type PtyBindingPersistenceOperationsRuntime = Pick<
StoreRuntimeState,
| 'flushOrThrow'
| 'lastDurableWriteGeneration'
| 'pendingWrite'
| 'quitFlushStarted'
| 'state'
| 'writeGeneration'
| 'writeTimer'
>
type PersistPtyBindingArgs = {
worktreeId: string
tabId: string
leafId: string
ptyId: string
incarnationId?: string
startupCwd?: string
expectedBinding?: { ptyId: string; incarnationId?: string }
expectedSourceBinding?: PtyBindingSourceExpectation
/** Set by host-initiated creates, which have no renderer session writer behind them. */
hostAdmittedMembership?: boolean
/**
* Defaults true, which is what `pty:spawn` needs — it can beat the debounced layout writer
* and must be able to mint the surface it is binding. A reattach is the opposite: the pane
* either still exists or the user closed it, so creating one grafts back a tab they closed.
* Callers pass false only once absence is meaningful; see the relay's reattach bind.
*/
mayCreate?: boolean
/** Reattach must not revive a surface a prior build durably recorded as retired. */
mayReviveRetiredSurface?: boolean
/** Span metadata only; see `PtyBindingOrigin`. The write path never reads it. */
origin?: PtyBindingOrigin
}
const ptyBindingPersistenceOperationsContext = Symbol('PtyBindingPersistenceOperations')
type PtyBindingPersistenceOperationsContext = {
@@ -34,235 +72,199 @@ export class PtyBindingPersistenceOperations {
this[ptyBindingPersistenceOperationsContext] = { runtime, sessions }
}
persistPtyBinding(
args: {
worktreeId: string
tabId: string
leafId: string
ptyId: string
incarnationId?: string
startupCwd?: string
expectedBinding?: { ptyId: string; incarnationId?: string }
expectedSourceBinding?: PtyBindingSourceExpectation
/** Set by host-initiated creates, which have no renderer session writer behind them. */
hostAdmittedMembership?: boolean
/**
* Defaults true, which is what `pty:spawn` needs — it can beat the debounced layout writer
* and must be able to mint the surface it is binding. A reattach is the opposite: the pane
* either still exists or the user closed it, so creating one grafts back a tab they closed.
* Callers pass false only once absence is meaningful; see the relay's reattach bind.
*/
mayCreate?: boolean
/** Reattach must not revive a surface a prior build durably recorded as retired. */
mayReviveRetiredSurface?: boolean
},
hostId?: string | null
): boolean {
persistPtyBinding(args: PersistPtyBindingArgs, hostId?: string | null): boolean {
const runtime = this[ptyBindingPersistenceOperationsContext].runtime
const resolvedHostId = resolveHostId(hostId)
const session =
this[ptyBindingPersistenceOperationsContext].sessions.getWorkspaceSession(resolvedHostId)
const paneKey = `${args.tabId}:${args.leafId}`
const bindingWorktreeId = args.expectedSourceBinding?.worktreeId ?? args.worktreeId
if (args.expectedSourceBinding) {
const expected = args.expectedSourceBinding
if (expected.tabId !== args.tabId) {
return false
}
const sourceTab = session.tabsByWorktree?.[bindingWorktreeId]?.find(
(candidate) => candidate.id === expected.tabId && candidate.worktreeId === bindingWorktreeId
)
const sourceLayout = session.terminalLayoutsByTabId?.[expected.tabId]
const sourcePaneKey = `${expected.tabId}:${expected.leafId}`
if (
!sourceTab ||
sourceLayout?.ptyIdsByLeafId?.[expected.leafId] !== expected.ptyId ||
!layoutContainsLeafId(sourceLayout.root, expected.leafId) ||
(expected.incarnationId !== undefined &&
session.terminalPtyIncarnationsByPaneKey?.[sourcePaneKey] !== expected.incarnationId)
) {
return false
}
}
if (args.expectedBinding) {
const tab = session.tabsByWorktree?.[bindingWorktreeId]?.find(
(candidate) => candidate.id === args.tabId && candidate.worktreeId === bindingWorktreeId
)
const boundPtyId = session.terminalLayoutsByTabId?.[args.tabId]?.ptyIdsByLeafId?.[args.leafId]
if (
!tab ||
boundPtyId !== args.expectedBinding.ptyId ||
session.terminalPtyIncarnationsByPaneKey?.[paneKey] !== args.expectedBinding.incarnationId
) {
return false
}
}
// Decided before any mutation so a refusal leaves nothing half-written. Mirrors the four
// creating branches below — mint a tab, mint a root leaf, split the root and graft a leaf,
// mint a layout — each of which sets `terminalMembershipChanged`.
if (
args.mayReviveRetiredSurface === false &&
session.terminalSurfaceTombstonesByPaneKey?.[paneKey]
) {
const span = startPtyBindingSpan({
hostKind: parseExecutionHostId(resolvedHostId)?.kind ?? 'local',
origin: args.origin ?? 'unknown',
savePending: runtime.writeTimer !== null || runtime.pendingWrite !== null,
generationGap: runtime.writeGeneration - runtime.lastDurableWriteGeneration
})
if (ptyBindingIsRefused(args, session, bindingWorktreeId, paneKey)) {
span.finish('refused')
return false
}
if (args.mayCreate === false) {
const existingTab = session.tabsByWorktree?.[bindingWorktreeId]?.find(
(candidate) => candidate.id === args.tabId
)
const existingLayout = session.terminalLayoutsByTabId?.[args.tabId]
const wouldCreateTopology =
!existingTab ||
(isTerminalLeafId(args.leafId) &&
(!existingLayout ||
!existingLayout.root ||
!layoutContainsLeafId(existingLayout.root, args.leafId)))
if (wouldCreateTopology) {
return false
}
// A durable reattach needs neither a session clone nor whole-state serialization.
const verdict = evaluatePtyBindingFastLane(
args,
session,
bindingWorktreeId,
!runtime.quitFlushStarted && runtime.lastDurableWriteGeneration >= runtime.writeGeneration
)
span.setEligibility(verdict)
if (verdict.eligible) {
span.finish('fast_lane')
return true
}
try {
writePtyBinding(this, args, session, resolvedHostId, bindingWorktreeId, paneKey)
} catch (err) {
span.finish('threw', err)
throw err
}
span.finish('flushed')
return true
}
}
function writePtyBinding(
owner: PtyBindingPersistenceOperations,
args: PersistPtyBindingArgs,
session: WorkspaceSessionState,
resolvedHostId: ReturnType<typeof resolveHostId>,
bindingWorktreeId: string,
paneKey: string
): void {
const runtime = owner[ptyBindingPersistenceOperationsContext].runtime
const sessionBeforeBinding = cloneWorkspaceSessionState(session)
try {
if (resolvedHostId !== LOCAL_EXECUTION_HOST_ID) {
this[ptyBindingPersistenceOperationsContext].runtime.state.workspaceSessionsByHostId = {
...this[ptyBindingPersistenceOperationsContext].runtime.state.workspaceSessionsByHostId,
runtime.state.workspaceSessionsByHostId = {
...runtime.state.workspaceSessionsByHostId,
[resolvedHostId]: session
}
}
const sessionBeforeBinding = cloneWorkspaceSessionState(session)
const reconciledIncarnation =
args.expectedBinding !== undefined &&
args.incarnationId !== args.expectedBinding.incarnationId
let terminalMembershipChanged = false
let hostAdmittedTabCreated = false
const advanceTopologyFence = (): void => {
const repoId = getRepoIdFromWorktreeId(bindingWorktreeId)
const currentRevision = session.terminalTopologyRevisionByRepoId?.[repoId] ?? 0
// Why: a split, or a host-admitted tab the renderer has never seen, is itself
// the authority — with no fence the renderer's pre-create tab list replays
// over it and the tab is lost even on the repo's first such change.
const establishesMembershipAuthority =
args.expectedSourceBinding !== undefined || hostAdmittedTabCreated
if (
!reconciledIncarnation &&
(!terminalMembershipChanged || (currentRevision <= 0 && !establishesMembershipAuthority))
) {
return
}
// Why: host-admitted membership or incarnation changes must outrank a stale renderer replay.
session.terminalTopologyRevisionByRepoId = {
...session.terminalTopologyRevisionByRepoId,
[repoId]: currentRevision + 1
}
}
const restoreSession = (): void => {
if (resolvedHostId === LOCAL_EXECUTION_HOST_ID) {
this[ptyBindingPersistenceOperationsContext].runtime.state.workspaceSession =
sessionBeforeBinding
} else {
this[ptyBindingPersistenceOperationsContext].runtime.state.workspaceSessionsByHostId = {
...this[ptyBindingPersistenceOperationsContext].runtime.state.workspaceSessionsByHostId,
[resolvedHostId]: sessionBeforeBinding
}
}
}
if (args.incarnationId) {
session.terminalPtyIncarnationsByPaneKey = {
...session.terminalPtyIncarnationsByPaneKey,
[paneKey]: args.incarnationId
}
if (session.terminalSurfaceTombstonesByPaneKey?.[paneKey]) {
session.terminalSurfaceTombstonesByPaneKey = {
...session.terminalSurfaceTombstonesByPaneKey
}
delete session.terminalSurfaceTombstonesByPaneKey[paneKey]
}
}
const tabs = session.tabsByWorktree?.[bindingWorktreeId]
const tab = tabs?.find((t) => t.id === args.tabId)
if (tab) {
tab.ptyId = args.ptyId
applyPtyBinding(args, session, bindingWorktreeId, paneKey)
runtime.flushOrThrow()
} catch (err) {
if (resolvedHostId === LOCAL_EXECUTION_HOST_ID) {
runtime.state.workspaceSession = sessionBeforeBinding
} else {
terminalMembershipChanged = true
hostAdmittedTabCreated = args.hostAdmittedMembership === true
// Why: pty:spawn can beat the debounced writer; persist a minimal tab so hydration won't prune the binding as orphaned.
const nextTabs = [
...(tabs ?? []),
createMinimalPersistedTerminalTab({
...args,
worktreeId: bindingWorktreeId,
existingTabCount: tabs?.length ?? 0
})
]
session.tabsByWorktree = {
...session.tabsByWorktree,
[bindingWorktreeId]: nextTabs
}
session.activeWorktreeId ??= bindingWorktreeId
session.activeTabId ??= args.tabId
session.activeTabIdByWorktree = {
...session.activeTabIdByWorktree,
[bindingWorktreeId]: session.activeTabIdByWorktree?.[bindingWorktreeId] ?? args.tabId
runtime.state.workspaceSessionsByHostId = {
...runtime.state.workspaceSessionsByHostId,
[resolvedHostId]: sessionBeforeBinding
}
}
if (!isTerminalLeafId(args.leafId)) {
// Why: keep legacy renderer-local pane ids out of durable leaf-keyed layout state after the UUID migration.
advanceTopologyFence()
try {
this[ptyBindingPersistenceOperationsContext].runtime.flushOrThrow()
} catch (err) {
restoreSession()
throw err
}
return true
}
const layout = session.terminalLayoutsByTabId?.[args.tabId]
if (layout) {
if (!layout.root) {
terminalMembershipChanged = true
// Why: createTab can persist an empty layout before TerminalPane mounts; the sync binding still needs a durable root.
layout.root = { type: 'leaf', leafId: args.leafId }
layout.activeLeafId = args.leafId
layout.expandedLeafId = null
} else if (!layoutContainsLeafId(layout.root, args.leafId)) {
terminalMembershipChanged = true
// Why: splitPane spawns before its snapshot reaches main; add a minimal leaf so a crash can't strand the pane's binding.
layout.root = {
type: 'split',
direction: 'vertical',
first: cloneLayoutNode(layout.root),
second: { type: 'leaf', leafId: args.leafId }
}
layout.activeLeafId = args.leafId
if (layout.expandedLeafId && !layoutContainsLeafId(layout.root, layout.expandedLeafId)) {
layout.expandedLeafId = null
}
}
layout.ptyIdsByLeafId = {
...layout.ptyIdsByLeafId,
[args.leafId]: args.ptyId
}
} else {
terminalMembershipChanged = true
// Why: first tab spawn — persist a minimal layout so a SIGKILL before the renderer snapshot can't lose ptyIdsByLeafId.
session.terminalLayoutsByTabId = {
...session.terminalLayoutsByTabId,
[args.tabId]: {
root: { type: 'leaf', leafId: args.leafId },
activeLeafId: args.leafId,
expandedLeafId: null,
ptyIdsByLeafId: { [args.leafId]: args.ptyId }
}
}
}
advanceTopologyFence()
try {
this[ptyBindingPersistenceOperationsContext].runtime.flushOrThrow()
} catch (err) {
restoreSession()
throw err
}
return true
throw err
}
}
function applyPtyBinding(
args: PersistPtyBindingArgs,
session: WorkspaceSessionState,
bindingWorktreeId: string,
paneKey: string
): void {
const reconciledIncarnation =
args.expectedBinding !== undefined && args.incarnationId !== args.expectedBinding.incarnationId
let terminalMembershipChanged = false
let hostAdmittedTabCreated = false
const advanceTopologyFence = (): void => {
const repoId = getRepoIdFromWorktreeId(bindingWorktreeId)
const currentRevision = session.terminalTopologyRevisionByRepoId?.[repoId] ?? 0
// Why: a split, or a host-admitted tab the renderer has never seen, is itself
// the authority — with no fence the renderer's pre-create tab list replays
// over it and the tab is lost even on the repo's first such change.
const establishesMembershipAuthority =
args.expectedSourceBinding !== undefined || hostAdmittedTabCreated
if (
!reconciledIncarnation &&
(!terminalMembershipChanged || (currentRevision <= 0 && !establishesMembershipAuthority))
) {
return
}
// Why: host-admitted membership or incarnation changes must outrank a stale renderer replay.
session.terminalTopologyRevisionByRepoId = {
...session.terminalTopologyRevisionByRepoId,
[repoId]: currentRevision + 1
}
}
if (args.incarnationId) {
session.terminalPtyIncarnationsByPaneKey = {
...session.terminalPtyIncarnationsByPaneKey,
[paneKey]: args.incarnationId
}
if (session.terminalSurfaceTombstonesByPaneKey?.[paneKey]) {
session.terminalSurfaceTombstonesByPaneKey = {
...session.terminalSurfaceTombstonesByPaneKey
}
delete session.terminalSurfaceTombstonesByPaneKey[paneKey]
}
}
const tabs = session.tabsByWorktree?.[bindingWorktreeId]
const tab = tabs?.find((t) => t.id === args.tabId)
if (tab) {
tab.ptyId = tabRowPtyIdAfterLeafBinding(
tab,
session.terminalLayoutsByTabId?.[args.tabId]?.ptyIdsByLeafId,
args.leafId,
args.ptyId
)
} else {
terminalMembershipChanged = true
hostAdmittedTabCreated = args.hostAdmittedMembership === true
// Why: pty:spawn can beat the debounced writer; persist a minimal tab so hydration won't prune the binding as orphaned.
const nextTabs = [
...(tabs ?? []),
createMinimalPersistedTerminalTab({
...args,
worktreeId: bindingWorktreeId,
existingTabCount: tabs?.length ?? 0
})
]
session.tabsByWorktree = {
...session.tabsByWorktree,
[bindingWorktreeId]: nextTabs
}
session.activeWorktreeId ??= bindingWorktreeId
session.activeTabId ??= args.tabId
session.activeTabIdByWorktree = {
...session.activeTabIdByWorktree,
[bindingWorktreeId]: session.activeTabIdByWorktree?.[bindingWorktreeId] ?? args.tabId
}
}
if (!isTerminalLeafId(args.leafId)) {
// Why: keep legacy renderer-local pane ids out of durable leaf-keyed layout state after the UUID migration.
advanceTopologyFence()
return
}
const layout = session.terminalLayoutsByTabId?.[args.tabId]
if (layout) {
if (!layout.root) {
terminalMembershipChanged = true
// Why: createTab can persist an empty layout before TerminalPane mounts; the sync binding still needs a durable root.
layout.root = { type: 'leaf', leafId: args.leafId }
layout.activeLeafId = args.leafId
layout.expandedLeafId = null
} else if (!layoutContainsLeafId(layout.root, args.leafId)) {
terminalMembershipChanged = true
// Why: splitPane spawns before its snapshot reaches main; add a minimal leaf so a crash can't strand the pane's binding.
layout.root = {
type: 'split',
direction: 'vertical',
first: cloneLayoutNode(layout.root),
second: { type: 'leaf', leafId: args.leafId }
}
layout.activeLeafId = args.leafId
if (layout.expandedLeafId && !layoutContainsLeafId(layout.root, layout.expandedLeafId)) {
layout.expandedLeafId = null
}
}
layout.ptyIdsByLeafId = {
...layout.ptyIdsByLeafId,
[args.leafId]: args.ptyId
}
} else {
terminalMembershipChanged = true
// Why: first tab spawn — persist a minimal layout so a SIGKILL before the renderer snapshot can't lose ptyIdsByLeafId.
session.terminalLayoutsByTabId = {
...session.terminalLayoutsByTabId,
[args.tabId]: {
root: { type: 'leaf', leafId: args.leafId },
activeLeafId: args.leafId,
expandedLeafId: null,
ptyIdsByLeafId: { [args.leafId]: args.ptyId }
}
}
}
advanceTopologyFence()
}
export function installPtyBindingPersistenceOperationsContext(
target: object,
source: PtyBindingPersistenceOperations
@@ -0,0 +1,83 @@
import { isTerminalLeafId } from '../../../shared/stable-pane-id'
import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
import { layoutContainsLeafId } from '../restoring-sessions/terminal-layout-normalization'
import type { PtyBindingSourceExpectation } from './store'
export type PtyBindingRefusalRequest = {
tabId: string
leafId: string
expectedBinding?: { ptyId: string; incarnationId?: string }
expectedSourceBinding?: PtyBindingSourceExpectation
mayCreate?: boolean
mayReviveRetiredSurface?: boolean
}
/**
* The four fences a binding must clear before anything is mutated, so a refusal leaves nothing
* half-written. Order matters: every `false` here is returned before the write path or the
* fast lane can run, which is what the relay's lease expiry and the stable-owner throw rely on.
*/
export function ptyBindingIsRefused(
args: PtyBindingRefusalRequest,
session: WorkspaceSessionState,
bindingWorktreeId: string,
paneKey: string
): boolean {
if (args.expectedSourceBinding) {
const expected = args.expectedSourceBinding
if (expected.tabId !== args.tabId) {
return true
}
const sourceTab = session.tabsByWorktree?.[bindingWorktreeId]?.find(
(candidate) => candidate.id === expected.tabId && candidate.worktreeId === bindingWorktreeId
)
const sourceLayout = session.terminalLayoutsByTabId?.[expected.tabId]
const sourcePaneKey = `${expected.tabId}:${expected.leafId}`
if (
!sourceTab ||
sourceLayout?.ptyIdsByLeafId?.[expected.leafId] !== expected.ptyId ||
!layoutContainsLeafId(sourceLayout.root, expected.leafId) ||
(expected.incarnationId !== undefined &&
session.terminalPtyIncarnationsByPaneKey?.[sourcePaneKey] !== expected.incarnationId)
) {
return true
}
}
if (args.expectedBinding) {
const tab = session.tabsByWorktree?.[bindingWorktreeId]?.find(
(candidate) => candidate.id === args.tabId && candidate.worktreeId === bindingWorktreeId
)
const boundPtyId = session.terminalLayoutsByTabId?.[args.tabId]?.ptyIdsByLeafId?.[args.leafId]
if (
!tab ||
boundPtyId !== args.expectedBinding.ptyId ||
session.terminalPtyIncarnationsByPaneKey?.[paneKey] !== args.expectedBinding.incarnationId
) {
return true
}
}
// Mirrors the four creating branches of the write path — mint a tab, mint a root leaf, split
// the root and graft a leaf, mint a layout — each of which sets `terminalMembershipChanged`.
if (
args.mayReviveRetiredSurface === false &&
session.terminalSurfaceTombstonesByPaneKey?.[paneKey]
) {
return true
}
if (args.mayCreate === false) {
const existingTab = session.tabsByWorktree?.[bindingWorktreeId]?.find(
(candidate) => candidate.id === args.tabId
)
const existingLayout = session.terminalLayoutsByTabId?.[args.tabId]
const wouldCreateTopology =
!existingTab ||
(isTerminalLeafId(args.leafId) &&
(!existingLayout ||
!existingLayout.root ||
!layoutContainsLeafId(existingLayout.root, args.leafId)))
if (wouldCreateTopology) {
return true
}
}
return false
}
@@ -0,0 +1,107 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { _resetTracerForTests, setActiveSink } from '../../observability/tracer'
import {
_resetPtyBindingSpanSamplingForTests,
PTY_BINDING_FAST_LANE_SPAN_BUDGET_PER_WINDOW,
spawnCommitBindingOrigin,
startPtyBindingSpan
} from './pty-binding-span'
let records: unknown[]
beforeEach(() => {
records = []
setActiveSink({
push: (record) => {
records.push(record)
},
flush: () => {},
close: () => {}
})
_resetPtyBindingSpanSamplingForTests()
vi.useFakeTimers()
vi.setSystemTime(1_700_000_000_000)
})
afterEach(() => {
vi.useRealTimers()
_resetTracerForTests()
})
function finishFastLane(): void {
const span = startPtyBindingSpan({
hostKind: 'local',
origin: 'reattach',
savePending: false,
generationGap: 0
})
span.setEligibility({ eligible: true, misses: [] })
span.finish('fast_lane')
}
describe('persistence.pty-binding span', () => {
it('labels adopted relay bindings as reattach without an isReattach flag', () => {
expect(spawnCommitBindingOrigin({ agentSessionEnsure: { disposition: 'adopted' } })).toBe(
'reattach'
)
})
it('records the entry counters, eligibility, and outcome', () => {
const span = startPtyBindingSpan({
hostKind: 'ssh',
origin: 'spawn',
savePending: true,
generationGap: 2
})
span.setEligibility({ eligible: false, misses: ['tab_pty', 'not_durable'] })
span.finish('flushed')
expect(records).toHaveLength(1)
expect(records[0]).toHaveProperty('name', 'persistence.pty-binding')
expect(records[0]).toMatchObject({
attributes: {
'binding.host': 'ssh',
'binding.origin': 'spawn',
'binding.save_pending': true,
'binding.generation_gap': 2,
'binding.eligible': false,
'binding.misses': 'tab_pty,not_durable',
'binding.outcome': 'flushed'
}
})
})
it('records a throw as a failed span', () => {
const span = startPtyBindingSpan({
hostKind: 'local',
origin: 'reattach',
savePending: false,
generationGap: 0
})
span.finish('threw', new Error('disk full'))
expect(records[0]).toHaveProperty('exit._tag', 'Failure')
expect(records[0]).toHaveProperty(['attributes', 'binding.outcome'], 'threw')
})
it('caps fast-lane spans per window without dropping writes', () => {
for (let i = 0; i < PTY_BINDING_FAST_LANE_SPAN_BUDGET_PER_WINDOW + 5; i++) {
finishFastLane()
}
expect(records).toHaveLength(PTY_BINDING_FAST_LANE_SPAN_BUDGET_PER_WINDOW)
// Flushed spans are never dropped, even inside a saturated window.
const span = startPtyBindingSpan({
hostKind: 'local',
origin: 'reattach',
savePending: false,
generationGap: 0
})
span.finish('flushed')
expect(records).toHaveLength(PTY_BINDING_FAST_LANE_SPAN_BUDGET_PER_WINDOW + 1)
vi.setSystemTime(1_700_000_000_000 + 60_000)
finishFastLane()
expect(records).toHaveLength(PTY_BINDING_FAST_LANE_SPAN_BUDGET_PER_WINDOW + 2)
})
})
@@ -0,0 +1,92 @@
import { startSpan } from '../../observability/tracer'
import type { PtyBindingFastLaneMiss } from './pty-binding-fast-lane'
export type PtyBindingSpanOutcome = 'fast_lane' | 'flushed' | 'refused' | 'threw'
/**
* Who asked for the bind. `persistPtyBinding` cannot tell a fresh spawn from a warm remount, and
* fresh spawns always flush, so a rate over all calls understates the reattach hit rate. Metadata
* only: nothing in the write path may branch on it.
*/
export type PtyBindingOrigin = 'reattach' | 'spawn' | 'relay_reattach' | 'split' | 'unknown'
/** The spawn-commit paths share one rule: a split outranks a reattach, a reattach outranks a spawn. */
export function spawnCommitBindingOrigin(
commit: { isReattach?: boolean; agentSessionEnsure?: { disposition: string } },
expectedSourceBinding?: unknown
): PtyBindingOrigin {
if (expectedSourceBinding !== undefined) {
return 'split'
}
return commit.isReattach === true || commit.agentSessionEnsure?.disposition === 'adopted'
? 'reattach'
: 'spawn'
}
// Bound frequent no-op traces; writes, refusals, and failures are always recorded.
export const PTY_BINDING_FAST_LANE_SPAN_BUDGET_PER_WINDOW = 200
const FAST_LANE_WINDOW_MS = 60_000
let fastLaneWindow: { startMs: number; emitted: number } | null = null
function admitFastLaneSpan(nowMs: number): boolean {
if (!fastLaneWindow || nowMs - fastLaneWindow.startMs >= FAST_LANE_WINDOW_MS) {
fastLaneWindow = { startMs: nowMs, emitted: 0 }
}
if (fastLaneWindow.emitted >= PTY_BINDING_FAST_LANE_SPAN_BUDGET_PER_WINDOW) {
return false
}
fastLaneWindow.emitted += 1
return true
}
export type PtyBindingSpan = {
setEligibility(verdict: { eligible: boolean; misses: readonly PtyBindingFastLaneMiss[] }): void
finish(outcome: PtyBindingSpanOutcome, error?: unknown): void
}
/**
* One `persistence.pty-binding` span per `persistPtyBinding` call. Attributes are all
* low-cardinality on purpose: no pane key, PTY id, worktree id, path, or SSH target id ever lands
* in the trace file. A local-only NDJSON lane, collected only into a user-submitted bundle.
*/
export function startPtyBindingSpan(entry: {
hostKind: 'local' | 'ssh' | 'runtime'
origin: PtyBindingOrigin
savePending: boolean
generationGap: number
}): PtyBindingSpan {
const span = startSpan('persistence.pty-binding', {
attributes: {
kind: 'persistence',
'binding.host': entry.hostKind,
'binding.origin': entry.origin,
'binding.save_pending': entry.savePending,
'binding.generation_gap': entry.generationGap
},
shouldRecord(record) {
if (record.attributes['binding.outcome'] !== 'fast_lane') {
return true
}
return admitFastLaneSpan(Date.now())
}
})
return {
setEligibility(verdict) {
span.setAttribute('binding.eligible', verdict.eligible)
span.setAttribute('binding.misses', verdict.misses.join(','))
},
finish(outcome, error) {
span.setAttribute('binding.outcome', outcome)
if (outcome === 'threw') {
span.fail(error instanceof Error ? error : String(error))
return
}
span.end()
}
}
}
export function _resetPtyBindingSpanSamplingForTests(): void {
fastLaneWindow = null
}
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest'
import { tabRowPtyIdAfterLeafBinding } from './terminal-tab-pty-ownership'
const LEAF_A = 'leaf-a'
const LEAF_B = 'leaf-b'
describe('tabRowPtyIdAfterLeafBinding', () => {
it('fills a null row', () => {
expect(tabRowPtyIdAfterLeafBinding({ ptyId: null }, undefined, LEAF_A, 'pty-1')).toBe('pty-1')
expect(tabRowPtyIdAfterLeafBinding({ ptyId: null }, {}, LEAF_A, 'pty-1')).toBe('pty-1')
})
it('follows a respawn of the leaf the row already names', () => {
expect(
tabRowPtyIdAfterLeafBinding({ ptyId: 'pty-1' }, { [LEAF_A]: 'pty-1' }, LEAF_A, 'pty-1b')
).toBe('pty-1b')
})
it('leaves the row on the first pane when a sibling pane binds', () => {
expect(
tabRowPtyIdAfterLeafBinding(
{ ptyId: 'pty-1' },
{ [LEAF_A]: 'pty-1', [LEAF_B]: 'pty-2' },
LEAF_B,
'pty-2'
)
).toBe('pty-1')
// The sibling's first bind, before its leaf is in the map, must not steal the row either.
expect(
tabRowPtyIdAfterLeafBinding({ ptyId: 'pty-1' }, { [LEAF_A]: 'pty-1' }, LEAF_B, 'pty-2')
).toBe('pty-1')
})
it('preserves a non-null row until the renderer clears or replaces it', () => {
expect(
tabRowPtyIdAfterLeafBinding({ ptyId: 'pty-gone' }, { [LEAF_A]: 'pty-1' }, LEAF_B, 'pty-2')
).toBe('pty-gone')
expect(tabRowPtyIdAfterLeafBinding({ ptyId: 'pty-gone' }, undefined, LEAF_A, 'pty-1')).toBe(
'pty-gone'
)
})
})
@@ -0,0 +1,25 @@
import type { TerminalTab } from '../../../shared/terminal-tab-types'
type LeafPtyIds = Readonly<Record<string, string>> | undefined
/**
* A tab row names one PTY, but a split tab holds several panes. The renderer keeps the row on
* the first pane and refuses to let later split-pane spawns steal it (see terminal-pty-bindings.ts),
* because a remount reattaches the tab to whatever the row says. Main must agree, or every
* sibling pane's reattach rewrites the row and the two sides ping-pong forever.
*
* The row is rewritten only when it is null or points at the PTY this leaf is replacing.
* A missing leaf is not evidence that a non-null row can be reassigned.
*/
export function tabRowPtyIdAfterLeafBinding(
tab: Pick<TerminalTab, 'ptyId'>,
ptyIdsByLeafId: LeafPtyIds,
leafId: string,
ptyId: string
): string {
const current = tab.ptyId
if (current === null || current === ptyIdsByLeafId?.[leafId]) {
return ptyId
}
return current
}
@@ -445,7 +445,8 @@ describe('SshRelaySession reconnect incarnation ordering', () => {
leafId: INCARNATION_LEAF_ID,
ptyId: APP_PTY_ID,
incarnationId,
mayReviveRetiredSurface: false
mayReviveRetiredSurface: false,
origin: 'relay_reattach'
})
expect(vi.mocked(mockStore.persistPtyBinding).mock.invocationCallOrder[0]).toBeLessThan(
vi.mocked(mockStore.markSshRemotePtyLeasesAttachedAsync).mock.invocationCallOrder[0]!
+2 -1
View File
@@ -2765,7 +2765,8 @@ export class SshRelaySession {
ptyId: appPtyId,
incarnationId,
...(mayCreate ? {} : { mayCreate: false }),
mayReviveRetiredSurface: false
mayReviveRetiredSurface: false,
origin: 'relay_reattach'
})
if (bound === false) {
// Topology absence alone is not authority to kill a process, but neither refusal may