mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 16:09:43 +02:00
fix(terminal): recover a dropped output frame, do not merely schedule it
`_onSessionTerminal` drops an incoming frame once the app-owned render queues already hold 128KB. That is the right call — the alternative is an unbounded backlog — but a hole in a TUI byte stream is a desynced cursor, and a desynced cursor is muffled text (#464). The drop was only half of it. The recovery was a fire-and-forget timer: it nulled its own handle and then called `_onSessionNeedsRefresh()`, which opens with four early returns. Two of them — a buffer load already in flight, a refresh already owning this session — are MOST likely to be true during exactly the output burst that caused the drop. So the recovery was skipped precisely when it was needed, with nothing left to retry it, and the dropped bytes were never replayed. `_onSessionNeedsRefresh` reports whether it actually repainted now, and `_scheduleDroppedOutputRecovery` re-arms while it has not. Bounded by `DROP_RECOVERY_MAX_ATTEMPTS`, because every reason the refresh can be skipped is transient contention that clears in seconds and a permanently failing refresh must not become a loop against the API; giving up at the cap leaves exactly what the old code left, so the floor is no worse than before. The same 2s debounce still collapses a burst of drops into one attempt. This is the principle Ark0N established reviewing #431 for the WebSocket output-gap marker — only a repaint that actually happened settles the recovery — applied to the one recovery path that still trusted a timer having fired. The retry decision is a pure function in constants.js so the gate can reach it, and the scheduler itself is driven from app.js under a fake clock. The retry case and the no-retry case only pin the fix AS A PAIR: either alone passes against something wrong, one against the old fire-and-forget timer and the other against retrying forever. Checked by reverting app.js to the old shape, where three of the twelve fail. Two harness details that would otherwise have made the tests lie. The vm context baked in the real `setTimeout`, so `vi.useFakeTimers()` could not reach the scheduler and every case reported zero calls; it delegates lazily now. And app.js reached `CodemanDroppedOutput` as a bare global, which resolves in a browser but not in the vm — worth fixing beyond the test, because that call sits inside a timer where a ReferenceError is swallowed and would take the recovery with it. It reads through `window.` like terminal-ui.js does with its own constants. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e1e7dc5bd8
commit
00f022ccf8
@@ -0,0 +1,13 @@
|
||||
---
|
||||
"aicodeman": patch
|
||||
---
|
||||
|
||||
fix(terminal): a dropped output frame is recovered, not just scheduled for recovery
|
||||
|
||||
`_onSessionTerminal` drops an incoming frame once the app-owned render queues already hold 128KB. That is the right call — the alternative is an unbounded backlog — but a hole in a TUI byte stream is a desynced cursor, and a desynced cursor is muffled text (#464). The drop was only half of it.
|
||||
|
||||
The recovery was a fire-and-forget timer: it nulled its own handle and then called `_onSessionNeedsRefresh()`, which opens with four early returns. Two of those — a buffer load already in flight, a refresh already owning this session — are **most likely to be true during exactly the output burst that caused the drop**, so the recovery was skipped precisely when it was needed, with nothing left to retry it, and those bytes were never replayed.
|
||||
|
||||
`_onSessionNeedsRefresh` now reports whether it actually repainted, and `_scheduleDroppedOutputRecovery` re-arms while it has not. Bounded, because every reason the refresh can be skipped is transient contention that clears in seconds and a permanently failing refresh must not become a loop against the API — and giving up at the cap leaves exactly what the old code left, so the floor is no worse. The same 2s debounce still collapses a burst of drops into one attempt.
|
||||
|
||||
This is the principle the review of #431 established for the WebSocket output-gap marker — only a repaint that actually happened settles the recovery — applied to the one recovery path that still relied on a timer having fired.
|
||||
Reference in New Issue
Block a user