CommandCodeAI / CommandCodeAI/command-code
Bug: First prompt after suspend/resume hangs for seconds to minutes
Ninguém assumiu esta issue ainda.
- Linguagem predominante
- Sem dados de linguagem
- Estrelas
- 4k
- Forks
- 350
- Métricas de merge de PRs
- Nenhum PR com merge em 30d
Descrição
Summary
After a laptop suspend/resume cycle, the first prompt sent in an already-open cmd session stalls: the spinner runs, no tokens appear for anywhere from ~5 seconds to several minutes, then the turn either completes (displayed as "Worked for Xm Ys") or just hangs until i killed the process. Later prompts work normally. The request is being written into a pooled keep-alive connection whose network path died during sleep. The client never detects the stall and never retries.
Expected Behavior
The stale connection gets thrown away (or the request gets retried on a fresh one),
and the prompt answers at normal speed.
Actual Behavior
The app writes the request into a keep-alive connection whose network path died while the laptop was asleep. TCP on both ends still looks alive (no RST from anywhere), so the request just sits in the kernel's send queue while retransmits grind away with exponential backoff. The app never notices, no timeout fires, no retry, no error.
Eventually one of the retransmits punches through the revived path and the turn completes ("Worked for 1m 57s"). Measured time-to-first-token across three suspend cycles: 3.2s / 23.2s / 46.9s, scaling with how long I slept. Baseline is ~0s.
Steps to reproduce the issue
- Open
cmdin a terminal and have a little conversation with it (this leaves a keep-alive connection to the API sitting in the pool). - Suspend the laptop. 5 minutes is a reliable trigger.
- Wake up and immediately send a prompt.
- Watch the spinner run with 0 tokens for anywhere from ~5 seconds to several minutes.
Command Code Version
1.50.1
Operating System
Linux
Terminal/IDE
Ghostty, ran through Zed's integrated terminal tab.
Shell
fish
Session file (optional)
No response
Fix prompt (optional)
No response
Additional context
Went a bit overboard with instrumentation on this one, so here's the whole story for anyone investigating. Big caveat up front: this is one laptop, one home network, four trials, and I'm reading kernel counters off ss — so take my interpretations with a grain of salt. TL;DR at the bottom.
The setup
After a couple of "it hung again" episodes I wanted to try to catch one in the act, so before the next suspend I armed:
- Node inspector on the running
cmdprocess (SIGUSR1 → CDP websocket), so I could grab main-thread stacks while it was frozen - A socket monitor logging
ss -tnpoiefor that PID at 2 Hz, so I'd have a full TCP timeline across the suspend - A winsize logger on the terminal's pts (5 samples/sec), to check the "stale window size" theory
- A little CDP probe script that pauses the runtime, grabs 14 stack frames +
process.stdout.columns/rows+ active handles, then resumes
Specimen: command-code 1.50.1, Node v23.11.1, Arch Linux (zen 7.2.2), Hyprland/Wayland, Zed integrated terminal.
Trial 1 — short suspend: clean
Slept for ~8 seconds with a pooled connection up. Connection survived, request went out fine. First useful (if unsurprising) data point: short suspends seem to leave the path alive — it took a real sleep to trigger anything.
Trial 2 — 5-minute suspend: clean (and instructive)
Slept 5 minutes... and got instant TTFT. The monitor log suggested why: the pooled connection had already closed, roughly 50s after the last response, before I suspended. I sent the prompt a minute after waking, so the client opened a fresh connection — nothing stale to hit. So: the pool's idle lifetime after a response looks like ~55s (on my setup, at least), and the bug seems to need the connection to still be pooled at the moment of suspend.
Trial 3 — 5-minute suspend: HUNG (the interesting one)
Sent a quick prompt, suspended within that ~50s window, woke up, sent the real prompt — wedge. As far as I can reconstruct from the logs:
18:53:05 socket to Cloudflare edge ESTAB; response streams; pool goes idle
18:53:13 PRE-SUSPEND: ESTAB, Recv-Q 0, Send-Q 0, keepalive healthy ← precondition met
18:53:14 → 18:58:33 SUSPEND (319s)
18:58:33 resume. SAME socket (same local port, same inode 9627122) still ESTABLISHED.
My prompt (144 bytes) gets written into it... and appears to STICK in Send-Q.
18:58:33-38 Send-Q pinned at 144B, persist timer probing: 309ms → 477ms → 482ms backoff
18:59-19:00 retransmit grind continues
19:00:19 Send-Q finally drains — presumably a probe/retransmit punched through
19:00:40 journal finally records the turn (2m07s after I hit enter)
19:01:00 ss -i: bytes_retrans: 443 ≈ 3× the request size, RTT back to 43.9ms, streaming fine
The UI showed "Worked for 1m 57s". I never resized, never touched the pane, no errors anywhere.
What it was NOT (checked live, on the frozen process — this part I'm fairly confident in)
- Doesn't look like a renderer wedge: CDP stacks ×3 — main thread idles in
processTimers, not a singlecalculateLayoutframe. Event loop seemed healthy, just ticking spinner timers. - Doesn't look like a starving renderer:
Recv-Qstayed flat 0 the entire wedge — no bytes ever arrived from the server. - Not a spin loop: CPU delta 10 ticks / 3s (~3% of one core).
- Not a stale-winsize thing:
stdoutdims sane the whole time (53x60), SIGWINCH listeners normal. - My reading: the app is sitting on an active
TCPSocketWrapwith a pending read, waiting for a response that never comes, with the request stuck ~144 bytes deep in the kernel send queue. Happy to be corrected on the TCP details.
One honest caveat
The first time it recovered, recovery coincided with my CDP probing (each probe pauses/resumes the runtime, and a timer that expired during the pause fires in a burst on resume — which might abort+retry the request). I also can't rule out plain TCP backoff finally getting through on its own. So "what exactly ends the stall" is the one part I haven't nailed down, and my probing may have influenced it. It doesn't change the onset story, though — the request clearly lands in a zombie connection, and the app doesn't appear to notice.
Trial 4 — 5-minute suspend control: clean (and this might explain the intermittency)
Ran the identical experiment again. This time: instant TTFT. The monitor log shows the difference:
- Hang cycle: the zombie socket stayed silently
ESTABLISHEDafter resume → my request fell into it → minutes-long stall. - Clean cycle: the zombie died within 12 seconds of resume — looks like an RST (a silent keepalive failure would take minutes of probe retries, not 12s), though I can't fully rule out something else closing it — and it died before I sent anything → fresh connection → instant answer.
My best guess: the bug is a race — if the dead path answers with an RST, the pool recovers instantly; if the zombie stays half-alive, the first request falls into the void. Same suspend duration, opposite outcomes. That might be why this reproduces for some people/cycles and not others, though n=2 here, so, grain of salt.
Bonus oddity: the journal
The hung turn leaves no journal entry until recovery. When it finally lands, the user message and the assistant entry share the exact same millisecond timestamp — 2m07s after the prompt was actually sent. So it looks like the client only persists the turn once the request gets through.
TL;DR
Suspend seems to kill the network path behind the pooled keep-alive connection while the TCP endpoint itself survives, and no RST arrives to tell anyone. The first post-resume request gets written into this zombie connection, stalls in Send-Q, and the app never notices — no timeout, no retry, no error. Recovery is whatever TCP retransmits (or an external kick like a terminal resize) manages on its own, which would explain the 5s-to-minutes TTFT inflation scaling with suspend length. If that's roughly right, any of these would fix it: drain the undici pool on D-Bus PrepareForSleep, detect the suspend via the CLOCK_BOOTTIME/MONOTONIC gap, or just retry-once when no response headers arrive within ~5s. Even if my mechanism reading is off, a "no response headers in N seconds → new connection" fallback seems like it would paper over the whole class of problem.
Guia de contribuição
Nenhum guia de contribuição indexado para este repositório
Primeiros passos
- Leia a issue inteira e depois o guia de contribuição do projeto.
- Comente na issue dizendo que vai assumir — evita que duas pessoas façam o mesmo trabalho.
- Faça um fork do repositório e trabalhe em uma branch.
- Abra um pull request que referencie o número da issue.
Direção de pesquisa
Comece reproduzindo a sequência de suspensão/retomada com uma conexão keep-alive existente no pool e, em seguida, rastreie o ciclo de vida da requisição e o comportamento do pool em torno do primeiro prompt após a retomada. Está concluído quando uma conexão obsoleta é descartada ou a requisição é repetida em uma conexão nova, evitando a espera de vários minutos e restaurando o tempo normal até o primeiro token.
Escrita pelo modelo de indexação a partir do texto da issue.
Avaliação
- Stack de tecnologia
- node.js
- Domínio
- cli, networking
- Tipo de issue
- Bug
- Dificuldade
- 4/5
- Tempo estimado
- 3-5 dias
- Status de atividade
- Ativa
- Clareza
- Razoavelmente clara
- Facilidade para iniciantes
- 55/100