From 3ca0c988de7811fa3f614d7d15b047cad787e45f Mon Sep 17 00:00:00 2001 From: Forketyfork Date: Sat, 30 May 2026 20:14:59 +0200 Subject: [PATCH 1/2] fix(session): survive OSC 8 hyperlink capacity exhaustion Issue: terminal session dies when Codex emits too many OSC 8 hyperlinks. ghostty-vt starts with 4 hyperlinks per page; scroll operations that copy hyperlinked rows to full pages return HyperlinkSetOutOfMemory, which escaped through processOutput() and marked the session dead. Solution: catch HyperlinkSetOutOfMemory, HyperlinkSetNeedsRehash, and HyperlinkMapOutOfMemory inside processOutput() and log a warning instead of propagating them. Affected cells lose their hyperlink, but the session keeps running. ghostty-vt already uses this strategy in Terminal.print for the same class of failure. --- src/session/state.zig | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/session/state.zig b/src/session/state.zig index a1be2d9..7630528 100644 --- a/src/session/state.zig +++ b/src/session/state.zig @@ -594,7 +594,15 @@ pub const SessionState = struct { }; } const was_synchronized_output = self.synchronizedOutputActive(); - try stream.nextSlice(self.output_buf[0..n]); + stream.nextSlice(self.output_buf[0..n]) catch |err| switch (err) { + // Hyperlink capacity errors: drop the hyperlink but keep the session alive. + // ghostty-vt already uses this strategy inside Terminal.print. + error.HyperlinkSetOutOfMemory, + error.HyperlinkSetNeedsRehash, + error.HyperlinkMapOutOfMemory, + => log.warn("session {d}: OSC 8 hyperlink capacity exhausted, hyperlink dropped: {}", .{ self.id, err }), + else => return err, + }; const processed_at_ms = std.time.milliTimestamp(); self.updateSynchronizedOutputState(was_synchronized_output, processed_at_ms); self.markDirty(); From 4e6eb098ef4aa307f20c5c8553f5738e765ac320 Mon Sep 17 00:00:00 2001 From: Forketyfork Date: Sat, 30 May 2026 20:23:16 +0200 Subject: [PATCH 2/2] fix(session): process PTY buffer byte-by-byte to survive hyperlink errors Address review: nextSlice drops the unprocessed tail of the current PTY read when a hyperlink capacity error occurs mid-buffer. Switch to next(byte) so only the failing byte's side effect is lost; bytes after the failure point are still dispatched to the terminal. --- src/session/state.zig | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/session/state.zig b/src/session/state.zig index 7630528..59462c7 100644 --- a/src/session/state.zig +++ b/src/session/state.zig @@ -594,15 +594,18 @@ pub const SessionState = struct { }; } const was_synchronized_output = self.synchronizedOutputActive(); - stream.nextSlice(self.output_buf[0..n]) catch |err| switch (err) { - // Hyperlink capacity errors: drop the hyperlink but keep the session alive. - // ghostty-vt already uses this strategy inside Terminal.print. - error.HyperlinkSetOutOfMemory, - error.HyperlinkSetNeedsRehash, - error.HyperlinkMapOutOfMemory, - => log.warn("session {d}: OSC 8 hyperlink capacity exhausted, hyperlink dropped: {}", .{ self.id, err }), - else => return err, - }; + // Process byte-by-byte so a hyperlink capacity error on byte N doesn't + // silently discard bytes N+1..end of the PTY read. Hyperlink errors drop + // only that byte's side effect; normal output/control sequences continue. + for (self.output_buf[0..n]) |byte| { + stream.next(byte) catch |err| switch (err) { + error.HyperlinkSetOutOfMemory, + error.HyperlinkSetNeedsRehash, + error.HyperlinkMapOutOfMemory, + => log.warn("session {d}: OSC 8 hyperlink capacity exhausted, hyperlink dropped: {}", .{ self.id, err }), + else => return err, + }; + } const processed_at_ms = std.time.milliTimestamp(); self.updateSynchronizedOutputState(was_synchronized_output, processed_at_ms); self.markDirty();