fix(pty-connection): correct sequencing and state bugs in spawn and reat

- Fix terminal tail slice to take prefix instead of suffix, preserving escape
  sequence markers needed by next scan
- Clear pending pane serializer when direct SSH retry PTY is unclaimed
- Initialize interrupt status baseline to undefined so first input advances
  sequence counter
- Bump reattach generation only after confirming current attempt owns the stream,
  preventing superseded results from canceling in-flight prepaint
This commit is contained in:
Jinjing
2026-08-22 14:44:51 -07:00
parent fd7caa7e8d
commit cf1558d82b
4 changed files with 19 additions and 4 deletions
@@ -85,7 +85,9 @@ export function bindFreshSpawnFollowReset(session: ConnectPanePtySession): void
return ''
}
}
return tail.slice(-TERMINAL_RENDERER_RISK_SCAN_TAIL_CHARS)
// Why: keep the head — a suffix slice of an oversized parameter run drops
// the `\x1b[` the next scan needs to recognize the carried sequence.
return tail.slice(0, TERMINAL_RENDERER_RISK_SCAN_TAIL_CHARS)
}
session.foregroundRendererRiskOutputPrefersRenderRefresh = function (data: string): boolean {
@@ -114,6 +114,12 @@ export function bindStartFreshSpawn(session: ConnectPanePtySession): void {
: session.transport.getPtyId()
if (resolvedPtyId && !session.claimCapturedDirectSshRetryPty(resolvedPtyId)) {
session.finishReattachLiveDataDeferral(false, outputCallbacks.generation)
// Why: an outstanding declare keeps main's cooperation gate suppressing
// this paneKey's daemon-snapshot seed until something releases it.
const gen = await preSignalPromise
if (typeof gen === 'number') {
void window.api.pty.clearPendingPaneSerializer(session.cacheKey, gen).catch(() => {})
}
return null
}
const connectResult =
@@ -193,7 +199,9 @@ export function bindStartFreshSpawn(session: ConnectPanePtySession): void {
session.reconcilePtySizeAfterSpawn(resolvedPtyId, session.cols, session.rows)
}
const gen = await preSignalPromise
if (resolvedPtyId && (typeof gen === 'number' || isRemoteRuntimePtyId(resolvedPtyId))) {
// Why: a bound PTY owns the renderer serializer even when the declare was
// rejected; the gen token only settles or clears the pending declaration.
if (resolvedPtyId) {
if (!isRemoteRuntimePtyId(resolvedPtyId) || !hasPtySerializer(resolvedPtyId)) {
session.registerPaneSerializerFor(resolvedPtyId)
}
@@ -162,7 +162,10 @@ export function installInterruptInputIntent(session: ConnectPanePtySession): voi
session.questionAnsweredInference.observeSentTerminalInput(data)
}
session.pendingTerminalInputWrite = null
session.sequencedInterruptStatusBaseline = 0
// Why: an unset baseline must differ from the first `?? null` snapshot so the
// first acknowledged input still advances the sequence.
session.sequencedInterruptStatusBaseline = undefined
session.interruptStatusBaselineSequence = 0
session.setPendingTerminalInputWrite = (promise: Promise<boolean | null>): void => {
session.pendingTerminalInputWrite = promise
void promise.finally(() => {
@@ -57,13 +57,15 @@ export function bindHandleReattachResult(sessionBag: ConnectPanePtySession): voi
coldRestoreStartup?: ColdRestoreAgentResumeStartup | null,
attemptGeneration = session.transportStreamGeneration
): Promise<boolean> => {
session.authoritativeReattachGeneration += 1
if (session.disposed) {
return false
}
if (attemptGeneration !== session.transportStreamGeneration) {
return false
}
// Why: bump only once this attempt owns the stream, or a superseded result
// would cancel the current attempt's in-flight snapshot prepaint.
session.authoritativeReattachGeneration += 1
const connectResult =
result && typeof result === 'object' && 'id' in result ? (result as PtyConnectResult) : null