CommandCodeAI / CommandCodeAI/command-code
Bug: First prompt after suspend/resume hangs for seconds to minutes
まだ誰も着手していません。
- 主要言語
- 言語のデータがありません
- スター
- 4k
- フォーク
- 350
- PR マージ指標
- 30日以内にマージされた PR はありません
説明
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.
コントリビューションガイド
このリポジトリのコントリビューションガイドは索引されていません
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
調査の方向性
まず、既存のプール済み keep-alive 接続を使ってサスペンド/レジュームのシーケンスを再現し、その後、レジューム後最初の prompt 前後におけるリクエストのライフサイクルとプールの動作を追跡します。完了の条件は、古い接続が破棄されるか、リクエストが新しい接続で再試行され、数分間の停止を回避し、最初のトークンまでの通常の時間が復元されることです。
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- node.js
- 領域
- cli, networking
- issue の種類
- バグ
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 活発さ
- 活発
- 明瞭さ
- おおむね明確
- 初心者へのやさしさ
- 55/100