@@ -24,6 +24,11 @@ var feedPool = sync.Pool{
2424 },
2525}
2626
27+ type feedItem struct {
28+ data * []byte
29+ seq uint64
30+ }
31+
2732// All fields are owned exclusively by the session's run loop.
2833type sessionClient struct {
2934 id string
@@ -95,13 +100,14 @@ type Session struct {
95100 ptmx * os.File
96101 cmd * exec.Cmd
97102 term * libghostty.Terminal
98- feedCh chan * [] byte
103+ feedCh chan feedItem
99104 tempDir string
100105
101- actions chan any
102- ptyOut chan []byte
103- done chan struct {}
104- exitCode int32
106+ actions chan any
107+ ptyOut chan []byte
108+ done chan struct {}
109+ exitCode int32
110+ feedApplied atomic.Uint64
105111
106112 // sizeVal packs cols|rows as (cols<<16)|rows for lock-free reads.
107113 sizeVal atomic.Uint32
@@ -191,7 +197,7 @@ func newSession(ctx context.Context, name string, command []string, env []string
191197 ptmx : ptmx ,
192198 cmd : cmd ,
193199 term : term ,
194- feedCh : make (chan * [] byte , 64 ),
200+ feedCh : make (chan feedItem , 64 ),
195201 tempDir : tempDir ,
196202 actions : make (chan any , 16 ),
197203 ptyOut : make (chan []byte , 64 ),
@@ -270,7 +276,7 @@ func restoreSession(ctx context.Context, name string, command []string, env []st
270276 ptmx : ptmx ,
271277 cmd : cmd ,
272278 term : term ,
273- feedCh : make (chan * [] byte , 64 ),
279+ feedCh : make (chan feedItem , 64 ),
274280 tempDir : tempDir ,
275281 actions : make (chan any , 16 ),
276282 ptyOut : make (chan []byte , 64 ),
@@ -287,12 +293,20 @@ func restoreSession(ctx context.Context, name string, command []string, env []st
287293}
288294
289295func (s * Session ) feedLoop (ctx context.Context ) {
290- for bp := range s .feedCh {
291- if err := s .term .Feed (ctx , * bp ); err != nil {
296+ for item := range s .feedCh {
297+ if err := s .term .Feed (ctx , * item . data ); err != nil {
292298 slog .Debug ("wasm feed error" , "session" , s .Name , "err" , err )
293299 }
294- * bp = (* bp )[:cap (* bp )]
295- feedPool .Put (bp )
300+ s .feedApplied .Store (item .seq )
301+ * item .data = (* item .data )[:cap (* item .data )]
302+ feedPool .Put (item .data )
303+ }
304+ }
305+
306+ // waitFeedApplied blocks until feedLoop has applied every PTY chunk up to target.
307+ func (s * Session ) waitFeedApplied (target uint64 ) {
308+ for s .feedApplied .Load () < target {
309+ time .Sleep (100 * time .Microsecond )
296310 }
297311}
298312
@@ -331,16 +345,19 @@ func (s *Session) run() {
331345 // pendingFeed holds data waiting to be sent to feedCh. While
332346 // non-nil, we stop reading ptyOut (backpressure) but keep
333347 // processing actions so detach/kick/list don't stall.
334- var pendingFeed * []byte
348+ var pendingFeed * feedItem
349+ var nextFeedSeq uint64
335350
336351 for {
337352 // Nil-channel trick: only one of ptyCh/feedSend is active
338353 // at a time. When pendingFeed is nil, read ptyOut. When
339354 // non-nil, send to feedCh. Actions are always processed.
340355 var ptyCh <- chan []byte
341- var feedSend chan <- * []byte
356+ var feedSend chan <- feedItem
357+ var feedItemToSend feedItem
342358 if pendingFeed != nil {
343359 feedSend = s .feedCh
360+ feedItemToSend = * pendingFeed
344361 } else {
345362 ptyCh = s .ptyOut
346363 }
@@ -373,9 +390,10 @@ func (s *Session) run() {
373390 d := (* bp )[:len (data )]
374391 copy (d , data )
375392 * bp = d
376- pendingFeed = bp
393+ nextFeedSeq ++
394+ pendingFeed = & feedItem {data : bp , seq : nextFeedSeq }
377395
378- case feedSend <- pendingFeed :
396+ case feedSend <- feedItemToSend :
379397 pendingFeed = nil
380398
381399 case action := <- s .actions :
@@ -384,6 +402,12 @@ func (s *Session) run() {
384402 if ! a .readOnly {
385403 s .resizeForPending (clients , a .cols , a .rows , a .xpixel , a .ypixel )
386404 }
405+ if pendingFeed != nil {
406+ s .feedCh <- * pendingFeed
407+ pendingFeed = nil
408+ }
409+ // Attach dumps must reflect every PTY chunk we've already accepted.
410+ s .waitFeedApplied (nextFeedSeq )
387411
388412 dump , err := s .term .DumpScreen (s .ctx , libghostty .DumpVTFull )
389413 if err != nil {
@@ -485,7 +509,7 @@ func (s *Session) run() {
485509 // Clients see connection close (EOF), not Exited — this is
486510 // the kill/shutdown path.
487511 if pendingFeed != nil {
488- feedPool .Put (pendingFeed )
512+ feedPool .Put (pendingFeed . data )
489513 pendingFeed = nil
490514 }
491515 close (s .feedCh )
0 commit comments