Skip to content

Commit 69dcf1f

Browse files
author
Dan Shapiro
committed
fix startup probe replay-boundary cleanup
1 parent 995b4f8 commit 69dcf1f

File tree

2 files changed

+97
-13
lines changed

2 files changed

+97
-13
lines changed

src/components/TerminalView.tsx

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ const TRUNCATED_REPLAY_BYTES = 128 * 1024
103103

104104
type StartupProbeReplayDiscardState = {
105105
remainder: string | null
106+
buffered: string
106107
}
107108

108109
function resolveMinimumContrastRatio(theme?: { isDark?: boolean } | null): number {
@@ -111,25 +112,36 @@ function resolveMinimumContrastRatio(theme?: { isDark?: boolean } | null): numbe
111112

112113
function consumeStartupProbeReplayDiscard(raw: string, state: StartupProbeReplayDiscardState): string {
113114
const remainder = state.remainder
114-
state.remainder = null
115115
if (!remainder) {
116+
state.buffered = ''
116117
return raw
117118
}
118119

119-
let matched = 0
120+
let matched = state.buffered
121+
let index = 0
120122
while (
121-
matched < raw.length
122-
&& matched < remainder.length
123-
&& raw[matched] === remainder[matched]
123+
index < raw.length
124+
&& matched.length < remainder.length
125+
&& raw[index] === remainder[matched.length]
124126
) {
125-
matched += 1
127+
matched += raw[index]
128+
index += 1
126129
}
127130

128-
if (matched === remainder.length) {
129-
return raw.slice(matched)
131+
if (matched.length === remainder.length) {
132+
state.remainder = null
133+
state.buffered = ''
134+
return raw.slice(index)
130135
}
131136

132-
if (matched === raw.length) {
137+
if (index < raw.length) {
138+
state.remainder = null
139+
state.buffered = ''
140+
return `${matched}${raw.slice(index)}`
141+
}
142+
143+
if (index === raw.length) {
144+
state.buffered = matched
133145
return ''
134146
}
135147

@@ -318,6 +330,7 @@ export default function TerminalView({ tabId, paneId, paneContent, hidden }: Ter
318330
const startupProbeStateRef = useRef(createTerminalStartupProbeState())
319331
const startupProbeReplayDiscardStateRef = useRef<StartupProbeReplayDiscardState>({
320332
remainder: null,
333+
buffered: '',
321334
})
322335
const osc52ParserRef = useRef(createOsc52ParserState())
323336
const resolvedThemeRef = useRef(getTerminalTheme(settings.terminal.theme, settings.theme))
@@ -905,9 +918,10 @@ export default function TerminalView({ tabId, paneId, paneContent, hidden }: Ter
905918
const remainder = getStartupProbeReplayRemainder(pendingProbe)
906919
startupProbeReplayDiscardStateRef.current = {
907920
remainder,
921+
buffered: '',
908922
}
909923
} else {
910-
startupProbeReplayDiscardStateRef.current = { remainder: null }
924+
startupProbeReplayDiscardStateRef.current = { remainder: null, buffered: '' }
911925
}
912926
startupProbeStateRef.current = createTerminalStartupProbeState()
913927
}, [])
@@ -1855,7 +1869,11 @@ export default function TerminalView({ tabId, paneId, paneContent, hidden }: Ter
18551869
)
18561870
const enteringFreshLiveOutput = !frameOverlapsReplay
18571871
&& (Boolean(previousSeqState.pendingReplay) || previousSeqState.awaitingFreshSequence)
1858-
if (enteringFreshLiveOutput) {
1872+
if (
1873+
enteringFreshLiveOutput
1874+
&& !startupProbeReplayDiscardStateRef.current.remainder
1875+
&& !startupProbeReplayDiscardStateRef.current.buffered
1876+
) {
18591877
resetStartupProbeParser({ discardReplayRemainder: Boolean(previousSeqState.pendingReplay) })
18601878
}
18611879
raw = consumeStartupProbeReplayDiscard(raw, startupProbeReplayDiscardStateRef.current)

test/e2e/opencode-startup-probes.test.tsx

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ describe('opencode startup probes (e2e)', () => {
686686
])
687687
})
688688

689-
it('does not keep stale startup-probe cleanup active across multiple live frames', async () => {
689+
it('discards a stale startup-probe remainder even when it completes across two live frames', async () => {
690690
const terminalId = 'term-opencode-replay-gap-split-stale-remainder'
691691
const { attach } = await renderCreatedTerminal(terminalId)
692692
const replayFragment = OPEN_CODE_STARTUP_PROBE_FRAME.slice(0, -2)
@@ -745,7 +745,73 @@ describe('opencode startup probes (e2e)', () => {
745745

746746
await waitFor(() => {
747747
expect(writeEvents()).toEqual([
748-
{ kind: 'write', data: `${secondLiveFrame ?? ''}hello` },
748+
{ kind: 'write', data: 'hello' },
749+
])
750+
})
751+
752+
expect(sentMessages().filter((msg) => msg?.type === 'terminal.input')).toEqual([])
753+
})
754+
755+
it('restores buffered live bytes when a stale startup-probe remainder diverges on the next frame', async () => {
756+
const terminalId = 'term-opencode-replay-gap-stale-remainder-mismatch'
757+
const { attach } = await renderCreatedTerminal(terminalId)
758+
const replayFragment = OPEN_CODE_STARTUP_PROBE_FRAME.slice(0, -2)
759+
const [firstLiveFrame] = OPEN_CODE_STARTUP_PROBE_FRAME.slice(replayFragment.length).split('')
760+
761+
wsHarness.emit({
762+
type: 'terminal.attach.ready',
763+
terminalId,
764+
headSeq: 2,
765+
replayFromSeq: 1,
766+
replayToSeq: 2,
767+
attachRequestId: attach.attachRequestId,
768+
})
769+
770+
wsHarness.send.mockClear()
771+
ioEvents.length = 0
772+
773+
wsHarness.emit({
774+
type: 'terminal.output',
775+
terminalId,
776+
seqStart: 1,
777+
seqEnd: 1,
778+
data: replayFragment,
779+
attachRequestId: attach.attachRequestId,
780+
})
781+
782+
wsHarness.emit({
783+
type: 'terminal.output.gap',
784+
terminalId,
785+
fromSeq: 2,
786+
toSeq: 2,
787+
reason: 'replay_budget_exceeded',
788+
attachRequestId: attach.attachRequestId,
789+
})
790+
791+
wsHarness.emit({
792+
type: 'terminal.output',
793+
terminalId,
794+
seqStart: 3,
795+
seqEnd: 3,
796+
data: firstLiveFrame ?? '',
797+
attachRequestId: attach.attachRequestId,
798+
})
799+
800+
expect(sentMessages().filter((msg) => msg?.type === 'terminal.input')).toEqual([])
801+
expect(ioEvents).toEqual([])
802+
803+
wsHarness.emit({
804+
type: 'terminal.output',
805+
terminalId,
806+
seqStart: 4,
807+
seqEnd: 4,
808+
data: 'xhello',
809+
attachRequestId: attach.attachRequestId,
810+
})
811+
812+
await waitFor(() => {
813+
expect(writeEvents()).toEqual([
814+
{ kind: 'write', data: `${firstLiveFrame ?? ''}xhello` },
749815
])
750816
})
751817

0 commit comments

Comments
 (0)