Terminal replay can drop acknowledgements for live output and leave the PTY paused
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
VS Code 1.137.0 (`645f29cc3176500b4b5762ba887cf2a7f0ffdf2c`), macOS 26.2 arm64.
An integrated terminal stopped updating and accepting input after a window reload. The Codex CLI process had finished its response, but its UI thread was blocked in `write()`. A separate shell writing a bell to the same TTY was also blocked.
In the running pty host, this terminal had `_isPtyPaused = true`, `_unacknowledgedCharCount = 10549`, and its node-pty socket was not flowing. Calling `clearUnacknowledgedChars()` for that terminal restored both processes immediately. Other terminals remained readable.
I could not capture the original missing acknowledgements, but found a reproducible race in the replay path:
1. `PersistentTerminalProcess.triggerReplay()` fires the replay event, then calls `clearUnacknowledgedChars()`, allowing live output to resume.
2. `BasePty.handleReplay()` sets `_inReplay = true` while awaiting the replay's `writePromise`.
3. Live data delivered through `BasePty.handleData()` still reaches the renderer during that wait.
4. `LocalPty.acknowledgeDataEvent()` discards acknowledgements while `_inReplay` is true. This includes acknowledgements for live data, leaving the host's count inflated.
To reproduce at class level, instantiate `LocalPty` with a proxy that records acknowledgement calls and connect its data events to `AckDataBufferer`. Hold the replay event's `writePromise` unresolved and deliver 10,001 characters with `handleData()`. Flush the replay and live write callbacks in FIFO order in the same JavaScript task: resolve the replay promise, then acknowledge the live data before the async replay continuation runs.
Using `LocalPty`, `AckDataBufferer`, and the actual xterm write queue loaded by the shipped workbench, the proxy received no acknowledgements. With live data queued until replay completes, it received two 5,000-character acknowledgements and the data retained its order. A separate harness using the release's flow-control methods reproduced a permanently paused host: after another 95,000 characters were delivered and parsed, 10,001 unacknowledged characters remained, above the 5,000-character resume threshold.
The local fix buffers live `handleData()` events while `_inReplay` is true and delivers them in order after clearing that flag. The same harness then leaves one unacknowledged character and the host is not paused.
Relevant release sources: [BasePty](https://github.com/microsoft/vscode/blob/645f29cc3176500b4b5762ba887cf2a7f0ffdf2c/src/vs/workbench/contrib/terminal/common/basePty.ts), [LocalPty](https://github.com/microsoft/vscode/blob/645f29cc3176500b4b5762ba887cf2a7f0ffdf2c/src/vs/workbench/contrib/terminal/electron-browser/localPty.ts), [pty service](https://github.com/microsoft/vscode/blob/645f29cc3176500b4b5762ba887cf2a7f0ffdf2c/src/vs/platform/terminal/node/ptyService.ts).
The interactive incident has not been reproduced in a clean profile with extensions disabled. The class-level reproduction does not involve extensions. This installation also has local patches for cwd detection, Skia Graphite, and the separate `localPty` event-buffer leak; the replay and acknowledgement methods tested above were unchanged before this fix.
Contributor guide
Assessment
This issue has not been assessed yet.