Skip to content

fix(runtime): evict oldest queued PTY data instead of pausing the source - #1873

Merged
Astro-Han merged 3 commits into
mainfrom
fix/runtime-pty-final-frame-drain
Aug 2, 2026
Merged

fix(runtime): evict oldest queued PTY data instead of pausing the source#1873
Astro-Han merged 3 commits into
mainfrom
fix/runtime-pty-final-frame-drain

Conversation

@Astro-Han

Copy link
Copy Markdown
Contributor

Summary

Fixes the intermittent CI failure in shell-run-manager.test.js "drains the final frame after parser backpressure and drops an evicted wrapped prefix" (AssertionError: /FINAL-DRAIN/ with empty output). The test job failed on main twice today (runs 30730791871, 30729759095); both failures were this test.

Root cause: PtyScreenCollector paused the node-pty read stream when the parse queue exceeded its high-water mark (1 MiB). If the child process exited while the stream was still paused, node-pty destroyed the socket after 200 ms (DESTROY_SOCKET_TIMEOUT_MS) and the unread tail of the output — including the final frame — was lost before the exit event fired. The final snapshot then saw only the partially-parsed output stream, whose rows are all soft-wrapped continuations; sanitizeBuffer evicts the entire wrapped prefix, producing empty output with truncated: true. Status stayed completed because the lost bytes were never delivered, which is why only the text assertion failed.

Fix: backpressure now evicts the oldest queued data from a newest-first bounded queue instead of pausing the source. The source is never paused, so a fast-exiting child can never strand unread bytes behind node-pty's socket-destroy fence, and the final frame always reaches the terminal. This also matches the terminal's own newest-first semantics: scrollback eviction, wrapped-prefix eviction, and queue eviction are the same idea. Eviction marks the output truncated.

Design reasoning (first principles): pausing couples flow control (bounded memory) with delivery (complete data), and pause semantics conflict with process exit (exit means no more data will come, but a paused stream cannot deliver the final bytes within node-pty's destroy fence). Decoupling them — never pause the source, bound the parse queue, evict oldest — removes the race entirely.

Verification

  • New regression test PtyScreenCollector "keeps the newest frame and evicts the oldest queued data when the parse queue exceeds its budget": red on the old code (empty output), green on the new.
  • Existing integration test "drains the final frame after parser backpressure and drops an evicted wrapped prefix": passes locally (macOS). This is the test that fails on CI (Linux) — the race is timing-sensitive, so CI is the authoritative check.
  • node --test "dist/**/*.test.js" in packages/runtime: 2693 tests, 0 fail.
  • npm run lint, npm run format:check, npm run typecheck: clean.

Note: the CI failure could not be reproduced locally on macOS (timing differences), so the red-to-green signal for the integration test comes from CI; the new collector unit test locks the eviction semantics locally.

PtyScreenCollector paused the node-pty read stream when the parse queue
exceeded its high-water mark. If the child exited while the stream was
still paused, node-pty destroyed the socket after 200ms and the unread
tail (including the final frame) was lost before 'exit' fired, so the
final snapshot could be empty (CI: shell-run-manager 'drains the final
frame after parser backpressure'). Backpressure now evicts the oldest
queued data from a newest-first bounded queue, the source is never
paused, and the final frame always reaches the terminal. Eviction marks
the output truncated, matching scrollback eviction semantics.
Queue eviction removes bytes from the middle of the input stream. The
dropped chunk may have carried the terminator of an escape sequence the
xterm parser is still waiting on (an unterminated OSC/CSI/DCS), so the
retained suffix would be interpreted against stale parser state and
could be swallowed entirely — the newest frame again missing from the
final snapshot. Reset the terminal through RIS (ESC c) before the next
retained write: its full reset returns the parser to ground state and
empties the buffer, and the old screen is discarded with it since it
was built from incomplete input. historyTruncated stays set (eviction
itself is truncation).
Regression test drives an unterminated OSC into the terminal, evicts the
chunk carrying its BEL terminator, and asserts the newest frame survives.
Eviction removed an entry from the pending array and decremented the
byte counter, but the parse closure already chained on the sequence
promise still referenced the entry — and through it the full payload
string — until the chain reached it. With the source never paused, a
slow parse write stalls the chain while fresh chunks keep arriving, so
real memory could grow far beyond the queue budget even though the
counter stayed bounded (synchronous flood of 100 x 2 MiB chunks reached
~209 MiB heap vs. a 1 MiB budget). The closure now reads entry.data and
eviction clears it, releasing the payload immediately; only the small
entry object remains until the chain advances.
@Astro-Han
Astro-Han marked this pull request as ready for review August 2, 2026 04:51
@Astro-Han
Astro-Han merged commit 1b0827d into mainAug 2, 2026
5 checks passed
@Astro-Han
Astro-Han deleted the fix/runtime-pty-final-frame-drain branch August 2, 2026 04:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(runtime): evict oldest queued PTY data instead of pausing the source by Astro-Han · Pull Request #1873 · apache/maka · GitHub
Skip to content

fix(runtime): evict oldest queued PTY data instead of pausing the source - #1873

Merged
Astro-Han merged 3 commits into
mainfrom
fix/runtime-pty-final-frame-drain
Aug 2, 2026
Merged

fix(runtime): evict oldest queued PTY data instead of pausing the source#1873
Astro-Han merged 3 commits into
mainfrom
fix/runtime-pty-final-frame-drain

Conversation

@Astro-Han

Copy link
Copy Markdown
Contributor

Summary

Fixes the intermittent CI failure in shell-run-manager.test.js "drains the final frame after parser backpressure and drops an evicted wrapped prefix" (AssertionError: /FINAL-DRAIN/ with empty output). The test job failed on main twice today (runs 30730791871, 30729759095); both failures were this test.

Root cause: PtyScreenCollector paused the node-pty read stream when the parse queue exceeded its high-water mark (1 MiB). If the child process exited while the stream was still paused, node-pty destroyed the socket after 200 ms (DESTROY_SOCKET_TIMEOUT_MS) and the unread tail of the output — including the final frame — was lost before the exit event fired. The final snapshot then saw only the partially-parsed output stream, whose rows are all soft-wrapped continuations; sanitizeBuffer evicts the entire wrapped prefix, producing empty output with truncated: true. Status stayed completed because the lost bytes were never delivered, which is why only the text assertion failed.

Fix: backpressure now evicts the oldest queued data from a newest-first bounded queue instead of pausing the source. The source is never paused, so a fast-exiting child can never strand unread bytes behind node-pty's socket-destroy fence, and the final frame always reaches the terminal. This also matches the terminal's own newest-first semantics: scrollback eviction, wrapped-prefix eviction, and queue eviction are the same idea. Eviction marks the output truncated.

Design reasoning (first principles): pausing couples flow control (bounded memory) with delivery (complete data), and pause semantics conflict with process exit (exit means no more data will come, but a paused stream cannot deliver the final bytes within node-pty's destroy fence). Decoupling them — never pause the source, bound the parse queue, evict oldest — removes the race entirely.

Verification

  • New regression test PtyScreenCollector "keeps the newest frame and evicts the oldest queued data when the parse queue exceeds its budget": red on the old code (empty output), green on the new.
  • Existing integration test "drains the final frame after parser backpressure and drops an evicted wrapped prefix": passes locally (macOS). This is the test that fails on CI (Linux) — the race is timing-sensitive, so CI is the authoritative check.
  • node --test "dist/**/*.test.js" in packages/runtime: 2693 tests, 0 fail.
  • npm run lint, npm run format:check, npm run typecheck: clean.

Note: the CI failure could not be reproduced locally on macOS (timing differences), so the red-to-green signal for the integration test comes from CI; the new collector unit test locks the eviction semantics locally.

PtyScreenCollector paused the node-pty read stream when the parse queue
exceeded its high-water mark. If the child exited while the stream was
still paused, node-pty destroyed the socket after 200ms and the unread
tail (including the final frame) was lost before 'exit' fired, so the
final snapshot could be empty (CI: shell-run-manager 'drains the final
frame after parser backpressure'). Backpressure now evicts the oldest
queued data from a newest-first bounded queue, the source is never
paused, and the final frame always reaches the terminal. Eviction marks
the output truncated, matching scrollback eviction semantics.
Queue eviction removes bytes from the middle of the input stream. The
dropped chunk may have carried the terminator of an escape sequence the
xterm parser is still waiting on (an unterminated OSC/CSI/DCS), so the
retained suffix would be interpreted against stale parser state and
could be swallowed entirely — the newest frame again missing from the
final snapshot. Reset the terminal through RIS (ESC c) before the next
retained write: its full reset returns the parser to ground state and
empties the buffer, and the old screen is discarded with it since it
was built from incomplete input. historyTruncated stays set (eviction
itself is truncation).
Regression test drives an unterminated OSC into the terminal, evicts the
chunk carrying its BEL terminator, and asserts the newest frame survives.
Eviction removed an entry from the pending array and decremented the
byte counter, but the parse closure already chained on the sequence
promise still referenced the entry — and through it the full payload
string — until the chain reached it. With the source never paused, a
slow parse write stalls the chain while fresh chunks keep arriving, so
real memory could grow far beyond the queue budget even though the
counter stayed bounded (synchronous flood of 100 x 2 MiB chunks reached
~209 MiB heap vs. a 1 MiB budget). The closure now reads entry.data and
eviction clears it, releasing the payload immediately; only the small
entry object remains until the chain advances.
@Astro-Han
Astro-Han marked this pull request as ready for review August 2, 2026 04:51
@Astro-Han
Astro-Han merged commit 1b0827d into mainAug 2, 2026
5 checks passed
@Astro-Han
Astro-Han deleted the fix/runtime-pty-final-frame-drain branch August 2, 2026 04:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(runtime): evict oldest queued PTY data instead of pausing the source by Astro-Han · Pull Request #1873 · apache/maka · GitHub
Skip to content

fix(runtime): evict oldest queued PTY data instead of pausing the source - #1873

Merged
Astro-Han merged 3 commits into
mainfrom
fix/runtime-pty-final-frame-drain
Aug 2, 2026
Merged

fix(runtime): evict oldest queued PTY data instead of pausing the source#1873
Astro-Han merged 3 commits into
mainfrom
fix/runtime-pty-final-frame-drain

Conversation

@Astro-Han

Copy link
Copy Markdown
Contributor

Summary

Fixes the intermittent CI failure in shell-run-manager.test.js "drains the final frame after parser backpressure and drops an evicted wrapped prefix" (AssertionError: /FINAL-DRAIN/ with empty output). The test job failed on main twice today (runs 30730791871, 30729759095); both failures were this test.

Root cause: PtyScreenCollector paused the node-pty read stream when the parse queue exceeded its high-water mark (1 MiB). If the child process exited while the stream was still paused, node-pty destroyed the socket after 200 ms (DESTROY_SOCKET_TIMEOUT_MS) and the unread tail of the output — including the final frame — was lost before the exit event fired. The final snapshot then saw only the partially-parsed output stream, whose rows are all soft-wrapped continuations; sanitizeBuffer evicts the entire wrapped prefix, producing empty output with truncated: true. Status stayed completed because the lost bytes were never delivered, which is why only the text assertion failed.

Fix: backpressure now evicts the oldest queued data from a newest-first bounded queue instead of pausing the source. The source is never paused, so a fast-exiting child can never strand unread bytes behind node-pty's socket-destroy fence, and the final frame always reaches the terminal. This also matches the terminal's own newest-first semantics: scrollback eviction, wrapped-prefix eviction, and queue eviction are the same idea. Eviction marks the output truncated.

Design reasoning (first principles): pausing couples flow control (bounded memory) with delivery (complete data), and pause semantics conflict with process exit (exit means no more data will come, but a paused stream cannot deliver the final bytes within node-pty's destroy fence). Decoupling them — never pause the source, bound the parse queue, evict oldest — removes the race entirely.

Verification

  • New regression test PtyScreenCollector "keeps the newest frame and evicts the oldest queued data when the parse queue exceeds its budget": red on the old code (empty output), green on the new.
  • Existing integration test "drains the final frame after parser backpressure and drops an evicted wrapped prefix": passes locally (macOS). This is the test that fails on CI (Linux) — the race is timing-sensitive, so CI is the authoritative check.
  • node --test "dist/**/*.test.js" in packages/runtime: 2693 tests, 0 fail.
  • npm run lint, npm run format:check, npm run typecheck: clean.

Note: the CI failure could not be reproduced locally on macOS (timing differences), so the red-to-green signal for the integration test comes from CI; the new collector unit test locks the eviction semantics locally.

PtyScreenCollector paused the node-pty read stream when the parse queue
exceeded its high-water mark. If the child exited while the stream was
still paused, node-pty destroyed the socket after 200ms and the unread
tail (including the final frame) was lost before 'exit' fired, so the
final snapshot could be empty (CI: shell-run-manager 'drains the final
frame after parser backpressure'). Backpressure now evicts the oldest
queued data from a newest-first bounded queue, the source is never
paused, and the final frame always reaches the terminal. Eviction marks
the output truncated, matching scrollback eviction semantics.
Queue eviction removes bytes from the middle of the input stream. The
dropped chunk may have carried the terminator of an escape sequence the
xterm parser is still waiting on (an unterminated OSC/CSI/DCS), so the
retained suffix would be interpreted against stale parser state and
could be swallowed entirely — the newest frame again missing from the
final snapshot. Reset the terminal through RIS (ESC c) before the next
retained write: its full reset returns the parser to ground state and
empties the buffer, and the old screen is discarded with it since it
was built from incomplete input. historyTruncated stays set (eviction
itself is truncation).
Regression test drives an unterminated OSC into the terminal, evicts the
chunk carrying its BEL terminator, and asserts the newest frame survives.
Eviction removed an entry from the pending array and decremented the
byte counter, but the parse closure already chained on the sequence
promise still referenced the entry — and through it the full payload
string — until the chain reached it. With the source never paused, a
slow parse write stalls the chain while fresh chunks keep arriving, so
real memory could grow far beyond the queue budget even though the
counter stayed bounded (synchronous flood of 100 x 2 MiB chunks reached
~209 MiB heap vs. a 1 MiB budget). The closure now reads entry.data and
eviction clears it, releasing the payload immediately; only the small
entry object remains until the chain advances.
@Astro-Han
Astro-Han marked this pull request as ready for review August 2, 2026 04:51
@Astro-Han
Astro-Han merged commit 1b0827d into mainAug 2, 2026
5 checks passed
@Astro-Han
Astro-Han deleted the fix/runtime-pty-final-frame-drain branch August 2, 2026 04:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(runtime): evict oldest queued PTY data instead of pausing the source by Astro-Han · Pull Request #1873 · apache/maka · GitHub
Skip to content

fix(runtime): evict oldest queued PTY data instead of pausing the source - #1873

Merged
Astro-Han merged 3 commits into
mainfrom
fix/runtime-pty-final-frame-drain
Aug 2, 2026
Merged

fix(runtime): evict oldest queued PTY data instead of pausing the source#1873
Astro-Han merged 3 commits into
mainfrom
fix/runtime-pty-final-frame-drain

Conversation

@Astro-Han

Copy link
Copy Markdown
Contributor

Summary

Fixes the intermittent CI failure in shell-run-manager.test.js "drains the final frame after parser backpressure and drops an evicted wrapped prefix" (AssertionError: /FINAL-DRAIN/ with empty output). The test job failed on main twice today (runs 30730791871, 30729759095); both failures were this test.

Root cause: PtyScreenCollector paused the node-pty read stream when the parse queue exceeded its high-water mark (1 MiB). If the child process exited while the stream was still paused, node-pty destroyed the socket after 200 ms (DESTROY_SOCKET_TIMEOUT_MS) and the unread tail of the output — including the final frame — was lost before the exit event fired. The final snapshot then saw only the partially-parsed output stream, whose rows are all soft-wrapped continuations; sanitizeBuffer evicts the entire wrapped prefix, producing empty output with truncated: true. Status stayed completed because the lost bytes were never delivered, which is why only the text assertion failed.

Fix: backpressure now evicts the oldest queued data from a newest-first bounded queue instead of pausing the source. The source is never paused, so a fast-exiting child can never strand unread bytes behind node-pty's socket-destroy fence, and the final frame always reaches the terminal. This also matches the terminal's own newest-first semantics: scrollback eviction, wrapped-prefix eviction, and queue eviction are the same idea. Eviction marks the output truncated.

Design reasoning (first principles): pausing couples flow control (bounded memory) with delivery (complete data), and pause semantics conflict with process exit (exit means no more data will come, but a paused stream cannot deliver the final bytes within node-pty's destroy fence). Decoupling them — never pause the source, bound the parse queue, evict oldest — removes the race entirely.

Verification

  • New regression test PtyScreenCollector "keeps the newest frame and evicts the oldest queued data when the parse queue exceeds its budget": red on the old code (empty output), green on the new.
  • Existing integration test "drains the final frame after parser backpressure and drops an evicted wrapped prefix": passes locally (macOS). This is the test that fails on CI (Linux) — the race is timing-sensitive, so CI is the authoritative check.
  • node --test "dist/**/*.test.js" in packages/runtime: 2693 tests, 0 fail.
  • npm run lint, npm run format:check, npm run typecheck: clean.

Note: the CI failure could not be reproduced locally on macOS (timing differences), so the red-to-green signal for the integration test comes from CI; the new collector unit test locks the eviction semantics locally.

PtyScreenCollector paused the node-pty read stream when the parse queue
exceeded its high-water mark. If the child exited while the stream was
still paused, node-pty destroyed the socket after 200ms and the unread
tail (including the final frame) was lost before 'exit' fired, so the
final snapshot could be empty (CI: shell-run-manager 'drains the final
frame after parser backpressure'). Backpressure now evicts the oldest
queued data from a newest-first bounded queue, the source is never
paused, and the final frame always reaches the terminal. Eviction marks
the output truncated, matching scrollback eviction semantics.
Queue eviction removes bytes from the middle of the input stream. The
dropped chunk may have carried the terminator of an escape sequence the
xterm parser is still waiting on (an unterminated OSC/CSI/DCS), so the
retained suffix would be interpreted against stale parser state and
could be swallowed entirely — the newest frame again missing from the
final snapshot. Reset the terminal through RIS (ESC c) before the next
retained write: its full reset returns the parser to ground state and
empties the buffer, and the old screen is discarded with it since it
was built from incomplete input. historyTruncated stays set (eviction
itself is truncation).
Regression test drives an unterminated OSC into the terminal, evicts the
chunk carrying its BEL terminator, and asserts the newest frame survives.
Eviction removed an entry from the pending array and decremented the
byte counter, but the parse closure already chained on the sequence
promise still referenced the entry — and through it the full payload
string — until the chain reached it. With the source never paused, a
slow parse write stalls the chain while fresh chunks keep arriving, so
real memory could grow far beyond the queue budget even though the
counter stayed bounded (synchronous flood of 100 x 2 MiB chunks reached
~209 MiB heap vs. a 1 MiB budget). The closure now reads entry.data and
eviction clears it, releasing the payload immediately; only the small
entry object remains until the chain advances.
@Astro-Han
Astro-Han marked this pull request as ready for review August 2, 2026 04:51
@Astro-Han
Astro-Han merged commit 1b0827d into mainAug 2, 2026
5 checks passed
@Astro-Han
Astro-Han deleted the fix/runtime-pty-final-frame-drain branch August 2, 2026 04:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(runtime): evict oldest queued PTY data instead of pausing the source by Astro-Han · Pull Request #1873 · apache/maka · GitHub
Skip to content

fix(runtime): evict oldest queued PTY data instead of pausing the source - #1873

Merged
Astro-Han merged 3 commits into
mainfrom
fix/runtime-pty-final-frame-drain
Aug 2, 2026
Merged

fix(runtime): evict oldest queued PTY data instead of pausing the source#1873
Astro-Han merged 3 commits into
mainfrom
fix/runtime-pty-final-frame-drain

Conversation

@Astro-Han

Copy link
Copy Markdown
Contributor

Summary

Fixes the intermittent CI failure in shell-run-manager.test.js "drains the final frame after parser backpressure and drops an evicted wrapped prefix" (AssertionError: /FINAL-DRAIN/ with empty output). The test job failed on main twice today (runs 30730791871, 30729759095); both failures were this test.

Root cause: PtyScreenCollector paused the node-pty read stream when the parse queue exceeded its high-water mark (1 MiB). If the child process exited while the stream was still paused, node-pty destroyed the socket after 200 ms (DESTROY_SOCKET_TIMEOUT_MS) and the unread tail of the output — including the final frame — was lost before the exit event fired. The final snapshot then saw only the partially-parsed output stream, whose rows are all soft-wrapped continuations; sanitizeBuffer evicts the entire wrapped prefix, producing empty output with truncated: true. Status stayed completed because the lost bytes were never delivered, which is why only the text assertion failed.

Fix: backpressure now evicts the oldest queued data from a newest-first bounded queue instead of pausing the source. The source is never paused, so a fast-exiting child can never strand unread bytes behind node-pty's socket-destroy fence, and the final frame always reaches the terminal. This also matches the terminal's own newest-first semantics: scrollback eviction, wrapped-prefix eviction, and queue eviction are the same idea. Eviction marks the output truncated.

Design reasoning (first principles): pausing couples flow control (bounded memory) with delivery (complete data), and pause semantics conflict with process exit (exit means no more data will come, but a paused stream cannot deliver the final bytes within node-pty's destroy fence). Decoupling them — never pause the source, bound the parse queue, evict oldest — removes the race entirely.

Verification

  • New regression test PtyScreenCollector "keeps the newest frame and evicts the oldest queued data when the parse queue exceeds its budget": red on the old code (empty output), green on the new.
  • Existing integration test "drains the final frame after parser backpressure and drops an evicted wrapped prefix": passes locally (macOS). This is the test that fails on CI (Linux) — the race is timing-sensitive, so CI is the authoritative check.
  • node --test "dist/**/*.test.js" in packages/runtime: 2693 tests, 0 fail.
  • npm run lint, npm run format:check, npm run typecheck: clean.

Note: the CI failure could not be reproduced locally on macOS (timing differences), so the red-to-green signal for the integration test comes from CI; the new collector unit test locks the eviction semantics locally.

PtyScreenCollector paused the node-pty read stream when the parse queue
exceeded its high-water mark. If the child exited while the stream was
still paused, node-pty destroyed the socket after 200ms and the unread
tail (including the final frame) was lost before 'exit' fired, so the
final snapshot could be empty (CI: shell-run-manager 'drains the final
frame after parser backpressure'). Backpressure now evicts the oldest
queued data from a newest-first bounded queue, the source is never
paused, and the final frame always reaches the terminal. Eviction marks
the output truncated, matching scrollback eviction semantics.
Queue eviction removes bytes from the middle of the input stream. The
dropped chunk may have carried the terminator of an escape sequence the
xterm parser is still waiting on (an unterminated OSC/CSI/DCS), so the
retained suffix would be interpreted against stale parser state and
could be swallowed entirely — the newest frame again missing from the
final snapshot. Reset the terminal through RIS (ESC c) before the next
retained write: its full reset returns the parser to ground state and
empties the buffer, and the old screen is discarded with it since it
was built from incomplete input. historyTruncated stays set (eviction
itself is truncation).
Regression test drives an unterminated OSC into the terminal, evicts the
chunk carrying its BEL terminator, and asserts the newest frame survives.
Eviction removed an entry from the pending array and decremented the
byte counter, but the parse closure already chained on the sequence
promise still referenced the entry — and through it the full payload
string — until the chain reached it. With the source never paused, a
slow parse write stalls the chain while fresh chunks keep arriving, so
real memory could grow far beyond the queue budget even though the
counter stayed bounded (synchronous flood of 100 x 2 MiB chunks reached
~209 MiB heap vs. a 1 MiB budget). The closure now reads entry.data and
eviction clears it, releasing the payload immediately; only the small
entry object remains until the chain advances.
@Astro-Han
Astro-Han marked this pull request as ready for review August 2, 2026 04:51
@Astro-Han
Astro-Han merged commit 1b0827d into mainAug 2, 2026
5 checks passed
@Astro-Han
Astro-Han deleted the fix/runtime-pty-final-frame-drain branch August 2, 2026 04:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(runtime): evict oldest queued PTY data instead of pausing the source by Astro-Han · Pull Request #1873 · apache/maka · GitHub
Skip to content

fix(runtime): evict oldest queued PTY data instead of pausing the source - #1873

Merged
Astro-Han merged 3 commits into
mainfrom
fix/runtime-pty-final-frame-drain
Aug 2, 2026
Merged

fix(runtime): evict oldest queued PTY data instead of pausing the source#1873
Astro-Han merged 3 commits into
mainfrom
fix/runtime-pty-final-frame-drain

Conversation

@Astro-Han

Copy link
Copy Markdown
Contributor

Summary

Fixes the intermittent CI failure in shell-run-manager.test.js "drains the final frame after parser backpressure and drops an evicted wrapped prefix" (AssertionError: /FINAL-DRAIN/ with empty output). The test job failed on main twice today (runs 30730791871, 30729759095); both failures were this test.

Root cause: PtyScreenCollector paused the node-pty read stream when the parse queue exceeded its high-water mark (1 MiB). If the child process exited while the stream was still paused, node-pty destroyed the socket after 200 ms (DESTROY_SOCKET_TIMEOUT_MS) and the unread tail of the output — including the final frame — was lost before the exit event fired. The final snapshot then saw only the partially-parsed output stream, whose rows are all soft-wrapped continuations; sanitizeBuffer evicts the entire wrapped prefix, producing empty output with truncated: true. Status stayed completed because the lost bytes were never delivered, which is why only the text assertion failed.

Fix: backpressure now evicts the oldest queued data from a newest-first bounded queue instead of pausing the source. The source is never paused, so a fast-exiting child can never strand unread bytes behind node-pty's socket-destroy fence, and the final frame always reaches the terminal. This also matches the terminal's own newest-first semantics: scrollback eviction, wrapped-prefix eviction, and queue eviction are the same idea. Eviction marks the output truncated.

Design reasoning (first principles): pausing couples flow control (bounded memory) with delivery (complete data), and pause semantics conflict with process exit (exit means no more data will come, but a paused stream cannot deliver the final bytes within node-pty's destroy fence). Decoupling them — never pause the source, bound the parse queue, evict oldest — removes the race entirely.

Verification

  • New regression test PtyScreenCollector "keeps the newest frame and evicts the oldest queued data when the parse queue exceeds its budget": red on the old code (empty output), green on the new.
  • Existing integration test "drains the final frame after parser backpressure and drops an evicted wrapped prefix": passes locally (macOS). This is the test that fails on CI (Linux) — the race is timing-sensitive, so CI is the authoritative check.
  • node --test "dist/**/*.test.js" in packages/runtime: 2693 tests, 0 fail.
  • npm run lint, npm run format:check, npm run typecheck: clean.

Note: the CI failure could not be reproduced locally on macOS (timing differences), so the red-to-green signal for the integration test comes from CI; the new collector unit test locks the eviction semantics locally.

PtyScreenCollector paused the node-pty read stream when the parse queue
exceeded its high-water mark. If the child exited while the stream was
still paused, node-pty destroyed the socket after 200ms and the unread
tail (including the final frame) was lost before 'exit' fired, so the
final snapshot could be empty (CI: shell-run-manager 'drains the final
frame after parser backpressure'). Backpressure now evicts the oldest
queued data from a newest-first bounded queue, the source is never
paused, and the final frame always reaches the terminal. Eviction marks
the output truncated, matching scrollback eviction semantics.
Queue eviction removes bytes from the middle of the input stream. The
dropped chunk may have carried the terminator of an escape sequence the
xterm parser is still waiting on (an unterminated OSC/CSI/DCS), so the
retained suffix would be interpreted against stale parser state and
could be swallowed entirely — the newest frame again missing from the
final snapshot. Reset the terminal through RIS (ESC c) before the next
retained write: its full reset returns the parser to ground state and
empties the buffer, and the old screen is discarded with it since it
was built from incomplete input. historyTruncated stays set (eviction
itself is truncation).
Regression test drives an unterminated OSC into the terminal, evicts the
chunk carrying its BEL terminator, and asserts the newest frame survives.
Eviction removed an entry from the pending array and decremented the
byte counter, but the parse closure already chained on the sequence
promise still referenced the entry — and through it the full payload
string — until the chain reached it. With the source never paused, a
slow parse write stalls the chain while fresh chunks keep arriving, so
real memory could grow far beyond the queue budget even though the
counter stayed bounded (synchronous flood of 100 x 2 MiB chunks reached
~209 MiB heap vs. a 1 MiB budget). The closure now reads entry.data and
eviction clears it, releasing the payload immediately; only the small
entry object remains until the chain advances.
@Astro-Han
Astro-Han marked this pull request as ready for review August 2, 2026 04:51
@Astro-Han
Astro-Han merged commit 1b0827d into mainAug 2, 2026
5 checks passed
@Astro-Han
Astro-Han deleted the fix/runtime-pty-final-frame-drain branch August 2, 2026 04:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(runtime): evict oldest queued PTY data instead of pausing the source by Astro-Han · Pull Request #1873 · apache/maka · GitHub
Skip to content

fix(runtime): evict oldest queued PTY data instead of pausing the source - #1873

Merged
Astro-Han merged 3 commits into
mainfrom
fix/runtime-pty-final-frame-drain
Aug 2, 2026
Merged

fix(runtime): evict oldest queued PTY data instead of pausing the source#1873
Astro-Han merged 3 commits into
mainfrom
fix/runtime-pty-final-frame-drain

Conversation

@Astro-Han

Copy link
Copy Markdown
Contributor

Summary

Fixes the intermittent CI failure in shell-run-manager.test.js "drains the final frame after parser backpressure and drops an evicted wrapped prefix" (AssertionError: /FINAL-DRAIN/ with empty output). The test job failed on main twice today (runs 30730791871, 30729759095); both failures were this test.

Root cause: PtyScreenCollector paused the node-pty read stream when the parse queue exceeded its high-water mark (1 MiB). If the child process exited while the stream was still paused, node-pty destroyed the socket after 200 ms (DESTROY_SOCKET_TIMEOUT_MS) and the unread tail of the output — including the final frame — was lost before the exit event fired. The final snapshot then saw only the partially-parsed output stream, whose rows are all soft-wrapped continuations; sanitizeBuffer evicts the entire wrapped prefix, producing empty output with truncated: true. Status stayed completed because the lost bytes were never delivered, which is why only the text assertion failed.

Fix: backpressure now evicts the oldest queued data from a newest-first bounded queue instead of pausing the source. The source is never paused, so a fast-exiting child can never strand unread bytes behind node-pty's socket-destroy fence, and the final frame always reaches the terminal. This also matches the terminal's own newest-first semantics: scrollback eviction, wrapped-prefix eviction, and queue eviction are the same idea. Eviction marks the output truncated.

Design reasoning (first principles): pausing couples flow control (bounded memory) with delivery (complete data), and pause semantics conflict with process exit (exit means no more data will come, but a paused stream cannot deliver the final bytes within node-pty's destroy fence). Decoupling them — never pause the source, bound the parse queue, evict oldest — removes the race entirely.

Verification

  • New regression test PtyScreenCollector "keeps the newest frame and evicts the oldest queued data when the parse queue exceeds its budget": red on the old code (empty output), green on the new.
  • Existing integration test "drains the final frame after parser backpressure and drops an evicted wrapped prefix": passes locally (macOS). This is the test that fails on CI (Linux) — the race is timing-sensitive, so CI is the authoritative check.
  • node --test "dist/**/*.test.js" in packages/runtime: 2693 tests, 0 fail.
  • npm run lint, npm run format:check, npm run typecheck: clean.

Note: the CI failure could not be reproduced locally on macOS (timing differences), so the red-to-green signal for the integration test comes from CI; the new collector unit test locks the eviction semantics locally.

PtyScreenCollector paused the node-pty read stream when the parse queue
exceeded its high-water mark. If the child exited while the stream was
still paused, node-pty destroyed the socket after 200ms and the unread
tail (including the final frame) was lost before 'exit' fired, so the
final snapshot could be empty (CI: shell-run-manager 'drains the final
frame after parser backpressure'). Backpressure now evicts the oldest
queued data from a newest-first bounded queue, the source is never
paused, and the final frame always reaches the terminal. Eviction marks
the output truncated, matching scrollback eviction semantics.
Queue eviction removes bytes from the middle of the input stream. The
dropped chunk may have carried the terminator of an escape sequence the
xterm parser is still waiting on (an unterminated OSC/CSI/DCS), so the
retained suffix would be interpreted against stale parser state and
could be swallowed entirely — the newest frame again missing from the
final snapshot. Reset the terminal through RIS (ESC c) before the next
retained write: its full reset returns the parser to ground state and
empties the buffer, and the old screen is discarded with it since it
was built from incomplete input. historyTruncated stays set (eviction
itself is truncation).
Regression test drives an unterminated OSC into the terminal, evicts the
chunk carrying its BEL terminator, and asserts the newest frame survives.
Eviction removed an entry from the pending array and decremented the
byte counter, but the parse closure already chained on the sequence
promise still referenced the entry — and through it the full payload
string — until the chain reached it. With the source never paused, a
slow parse write stalls the chain while fresh chunks keep arriving, so
real memory could grow far beyond the queue budget even though the
counter stayed bounded (synchronous flood of 100 x 2 MiB chunks reached
~209 MiB heap vs. a 1 MiB budget). The closure now reads entry.data and
eviction clears it, releasing the payload immediately; only the small
entry object remains until the chain advances.
@Astro-Han
Astro-Han marked this pull request as ready for review August 2, 2026 04:51
@Astro-Han
Astro-Han merged commit 1b0827d into mainAug 2, 2026
5 checks passed
@Astro-Han
Astro-Han deleted the fix/runtime-pty-final-frame-drain branch August 2, 2026 04:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(runtime): evict oldest queued PTY data instead of pausing the source by Astro-Han · Pull Request #1873 · apache/maka · GitHub
Skip to content

fix(runtime): evict oldest queued PTY data instead of pausing the source - #1873

Merged
Astro-Han merged 3 commits into
mainfrom
fix/runtime-pty-final-frame-drain
Aug 2, 2026
Merged

fix(runtime): evict oldest queued PTY data instead of pausing the source#1873
Astro-Han merged 3 commits into
mainfrom
fix/runtime-pty-final-frame-drain

Conversation

@Astro-Han

Copy link
Copy Markdown
Contributor

Summary

Fixes the intermittent CI failure in shell-run-manager.test.js "drains the final frame after parser backpressure and drops an evicted wrapped prefix" (AssertionError: /FINAL-DRAIN/ with empty output). The test job failed on main twice today (runs 30730791871, 30729759095); both failures were this test.

Root cause: PtyScreenCollector paused the node-pty read stream when the parse queue exceeded its high-water mark (1 MiB). If the child process exited while the stream was still paused, node-pty destroyed the socket after 200 ms (DESTROY_SOCKET_TIMEOUT_MS) and the unread tail of the output — including the final frame — was lost before the exit event fired. The final snapshot then saw only the partially-parsed output stream, whose rows are all soft-wrapped continuations; sanitizeBuffer evicts the entire wrapped prefix, producing empty output with truncated: true. Status stayed completed because the lost bytes were never delivered, which is why only the text assertion failed.

Fix: backpressure now evicts the oldest queued data from a newest-first bounded queue instead of pausing the source. The source is never paused, so a fast-exiting child can never strand unread bytes behind node-pty's socket-destroy fence, and the final frame always reaches the terminal. This also matches the terminal's own newest-first semantics: scrollback eviction, wrapped-prefix eviction, and queue eviction are the same idea. Eviction marks the output truncated.

Design reasoning (first principles): pausing couples flow control (bounded memory) with delivery (complete data), and pause semantics conflict with process exit (exit means no more data will come, but a paused stream cannot deliver the final bytes within node-pty's destroy fence). Decoupling them — never pause the source, bound the parse queue, evict oldest — removes the race entirely.

Verification

  • New regression test PtyScreenCollector "keeps the newest frame and evicts the oldest queued data when the parse queue exceeds its budget": red on the old code (empty output), green on the new.
  • Existing integration test "drains the final frame after parser backpressure and drops an evicted wrapped prefix": passes locally (macOS). This is the test that fails on CI (Linux) — the race is timing-sensitive, so CI is the authoritative check.
  • node --test "dist/**/*.test.js" in packages/runtime: 2693 tests, 0 fail.
  • npm run lint, npm run format:check, npm run typecheck: clean.

Note: the CI failure could not be reproduced locally on macOS (timing differences), so the red-to-green signal for the integration test comes from CI; the new collector unit test locks the eviction semantics locally.

PtyScreenCollector paused the node-pty read stream when the parse queue
exceeded its high-water mark. If the child exited while the stream was
still paused, node-pty destroyed the socket after 200ms and the unread
tail (including the final frame) was lost before 'exit' fired, so the
final snapshot could be empty (CI: shell-run-manager 'drains the final
frame after parser backpressure'). Backpressure now evicts the oldest
queued data from a newest-first bounded queue, the source is never
paused, and the final frame always reaches the terminal. Eviction marks
the output truncated, matching scrollback eviction semantics.
Queue eviction removes bytes from the middle of the input stream. The
dropped chunk may have carried the terminator of an escape sequence the
xterm parser is still waiting on (an unterminated OSC/CSI/DCS), so the
retained suffix would be interpreted against stale parser state and
could be swallowed entirely — the newest frame again missing from the
final snapshot. Reset the terminal through RIS (ESC c) before the next
retained write: its full reset returns the parser to ground state and
empties the buffer, and the old screen is discarded with it since it
was built from incomplete input. historyTruncated stays set (eviction
itself is truncation).
Regression test drives an unterminated OSC into the terminal, evicts the
chunk carrying its BEL terminator, and asserts the newest frame survives.
Eviction removed an entry from the pending array and decremented the
byte counter, but the parse closure already chained on the sequence
promise still referenced the entry — and through it the full payload
string — until the chain reached it. With the source never paused, a
slow parse write stalls the chain while fresh chunks keep arriving, so
real memory could grow far beyond the queue budget even though the
counter stayed bounded (synchronous flood of 100 x 2 MiB chunks reached
~209 MiB heap vs. a 1 MiB budget). The closure now reads entry.data and
eviction clears it, releasing the payload immediately; only the small
entry object remains until the chain advances.
@Astro-Han
Astro-Han marked this pull request as ready for review August 2, 2026 04:51
@Astro-Han
Astro-Han merged commit 1b0827d into mainAug 2, 2026
5 checks passed
@Astro-Han
Astro-Han deleted the fix/runtime-pty-final-frame-drain branch August 2, 2026 04:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han