Cursor-agent CLI: TUI fails with `NGHTTP2_ENHANCE_YOUR_CALM` once skill payload exceeds ~11 MB; no scoping option

Where does the bug appear (feature/product)?

Cursor CLI

Describe the Bug

cursor-agent CLI’s TUI silently fails — every prompt produces an endless “Reconnecting…” spinner — when the combined size of all discovered SKILL.md files exceeds ~11 MB. Threshold is byte-based, not count-based: 220 small skills (50 KB each) and 32 large skills (1 MB each) both trigger it.

Underlying server rejection (visible only via agent --debug): NGHTTP2_ENHANCE_YOUR_CALM (HTTP/2 flow-control). The CLI retries 10 times then surfaces Connection failed repeatedly. With network.useHttp1ForAgent: true the same root cause surfaces more cleanly as Bad Request: Append data exceeds maximum size of 52428800 bytes against api2.cursor.sh.

The skill scanner uses recursive globs (**/.agents/skills/**/SKILL.md, **/.cursor/skills/**/SKILL.md, **/.claude/skills/**/SKILL.md, **/.codex/skills/**/SKILL.md, **/AGENTS.md, **/CLAUDE.md) and bundles every match into the per-turn request payload. There is no .cursorignore honoring on the skill loader, no env var, no config to scope skill discovery.

Multi-agent skill packs make this trivial to hit. gstack (GitHub - garrytan/gstack: Use Garry Tan's exact Claude Code setup: 23 opinionated tools that serve as CEO, Designer, Eng Manager, Release Manager, Doc Engineer, and QA · GitHub) ships every skill 9× — one canonical + per-runtime wrappers for .hermes, .openclaw, .opencode, .factory, .gbrain, .kiro, .slate, .cursor, .agents. A single gstack install with ~43 skills becomes ~390 SKILL.md (~28 MB) because the recursive glob matches every wrapper. On my machine: 1408 SKILL.md loaded, ~77 MB skill payload, TUI completely unusable.

Steps to Reproduce

  1. Plant 250 synthetic SKILL.md files of ~50 KB each:
mkdir -p ~/.agents/skills/_repro
for i in $(seq 1 250); do
  mkdir -p ~/.agents/skills/_repro/skill_$i
  python3 -c "print('---\nname: s$i\ndescription: x\n---\n' + 'x' * 49000)" \
    > ~/.agents/skills/_repro/skill_$i/SKILL.md
done
  1. Launch TUI with debug logs:
agent --debug --model composer-2.5
  1. Type any prompt (e.g. “what is 2 plus 2”) and press Enter
  2. Spinner shows “Composing” then UI freezes on “Reconnecting…”
  3. Inspect $TMPDIR/cursor-agent-debug-*/session.log:
AgentSkillsCursorRulesService load completed { ruleCount: 1408, skillCount: 1408 }
[nal_agent_retries] Initial request { attempt: 0 }
ConnectError: Stream closed with error code NGHTTP2_ENHANCE_YOUR_CALM
[nal_agent_retries] Error not retryable
turn.outcome { outcome: "error", error_code: "ERR_HTTP2_STREAM_ERROR",
               error_text: "Connection failed repeatedly" }
  1. Cleanup: rm -rf ~/.agents/skills/_repro

Threshold sweep (byte-based, not count-based)

Held baseline constant (14 plugin/cloud-rule skills, ~0.7 MB), varied N synthetic SKILL.md files of 50 KB each. Binary-searched the boundary:

Synthetic added Total skills Total skill bytes Result
205 217 ~10.85 MB pass
206 218 ~10.90 MB pass
207 219 ~10.95 MB pass
208 220 ~11.00 MB fail — NGHTTP2_ENHANCE_YOUR_CALM
275 287 ~14.4 MB fail
500 512 ~25.6 MB fail
32 × 1 MB 32 ~20 MB fail
62 × 1 MB 62 ~50 MB fail

Failure is byte-based: 32 × 1 MB (20 MB total) fails the same as 220 × 50 KB (11 MB total). Threshold sits at ~11 MB on the default HTTP/2 endpoint (agentn.global.api5.cursor.sh). A separate higher 50 MB hard limit exists on api2.cursor.sh (HTTP/1 path).

Expected Behavior

Either of these would fix it cleanly:

  1. cursor-agent honors .cursorignore (or a new .skillignore) in the SKILL discovery glob, so users can scope the scan. Currently ignoreService only gates file-read / grep operations — extend it (or add a parallel filter) into AgentSkillsCursorRulesService.loadSkillsFromDirectory and the recursive glob.

  2. cursor-agent supports a config option in ~/.cursor/cli-config.json to scope skill roots:

    {
    “agentSkillRoots”: [“~/.cursor/skills-cursor/”, “~/.agents/skills/gstack/”],
    “agentSkillIncludeWrappers”: false
    }

    Default behavior unchanged; opt-in for users who hit the limit.

  3. At minimum (UX fix): when NGHTTP2_ENHANCE_YOUR_CALM or the 50 MB BAD_REQUEST fires, the TUI surfaces “Your skill bundle is too large (NN MB > 11 MB). Run agent --skills-list to prune.” instead of looping silently on “Reconnecting”. Right now the only signal is the spinner forever, sending users down the wrong debugging path (network, auth, MCPs).

Operating System

MacOS

Version Information

CLI:
CLI Version 2026.05.24-dda726e
Model Composer 2.5
Subscription Tier Pro
OS darwin (arm64, macOS 15.4)

For AI issues: which model did you use?

used all models.

mainly → Composer 2.5

For AI issues: add Request ID with privacy disabled

Request ID: c8a20849-a240-49d4-8399-028f92a871d9

Note: this run was captured with privacyMode: 2 (ghost mode) enabled, so the server-side trace may be unavailable. Happy to re-run with privacy disabled if needed — just let me know.

Additional Information

Workarounds tried (none fix the underlying limit):

  1. .cursorignore at the skill root — ignored by the SKILL scanner. Only ignoreService for file reads honors it. Skill count unchanged.

  2. network.useHttp1ForAgent: true — switches endpoint to api2.cursor.sh and surfaces the cleaner 50 MB BAD_REQUEST error, but does not raise the per-stream allowance.

  3. Removing one runtime’s gstack wrappers (1408 → 974 skills) — still fails (~50 MB payload, way over 11 MB threshold).

  4. agent --print (headless) — happens to succeed more often because each invocation opens a fresh short-lived stream that survives flow-control more often. Not a fix; same payload is sent.

Effective workaround: physically delete SKILL.md files from disk until total bytes drop below ~11 MB. On my machine I pruned 1408 → 218 SKILL.md (77 MB → 9.28 MB) by removing per-runtime gstack wrappers — after that, TUI works (turn.outcome: outcome=success). But this is destructive and breaks skills the user wants available to other agent runtimes.

Related: This pattern also hits Claude Code (less severely — Claude Code drops skills gracefully with a “N skill descriptions dropped” warning instead of freezing). Filed a related issue on the gstack side asking them to stop shipping 9× duplicates: Skill files generated for other AI hosts pollute Claude Code's skill scanner · Issue #1694 · garrytan/gstack · GitHub

Even after gstack restructures, the Cursor-side ask stands: any recursive skill loader with no scoping mechanism is vulnerable to this from any sufficiently large skill pack.

Does this stop you from using Cursor

Yes - Cursor is unusable

Hey, thanks for the report. It’s one of the cleanest I’ve seen. The binary search on the threshold, the split between HTTP/2 vs HTTP/1 paths, and the note that .cursorignore isn’t honored by the skill scanner all match what we see in the code.

The root cause is known. AgentSkillsCursorRulesService recursively scans skill directories with no size cap, no scoping, and no ignore file filtering. Dedupe only works for hardcoded prefixes .cursor, .claude, .codex, .agents, so gstack wrappers like .hermes, .openclaw get treated as separate skills and can easily inflate the payload past the roughly 11 MB HTTP/2 limit.

We’re already tracking this issue on our side. I’ll add your details to the ticket: the exact threshold, the repro script, and the note about ignoreService. They’re helpful, especially the binary searched boundary. I can’t share an ETA for a fix yet.

No need to re-run with privacy off. The code review covers everything.

Current workaround, which you already found but for the record: prune SKILL.md files to keep the total under 11 MB. Pre-runtime gstack wrappers are the first things to remove, since the canonical skill will still be picked up from the main directory. Filing an issue with gstack about the 9x duplication is the right move, it fixes the amplification at the source.

Privacy mode privacyMode: 2 doesn’t affect this. The problem is in the client-side scan before the request is sent.

If I get an update on the fix, We’ll post it here.

Same NGHTTP2_ENHANCE_YOUR_CALM threshold, reached from @cursor/sdk instead of the CLI, with a payload users cannot prune. My long-lived local agent accumulated ~46 MB of checkpoint state across 3,635 blobs. Each turn now sends an 18.41 MiB POST to /agent.v1.AgentService/Run — over the measured ~11 MB boundary — and the server sends RST_STREAM code 11 in ~100ms. The SDK retries the identical payload ten times, then reports “Connection failed repeatedly”. Skills can be deleted; conversation history cannot, so the conversation is permanently bricked. Separately, collectPreFetchedBlobs defaults maxTotalBytes to 33554432, roughly 3× the HTTP/2 limit, so the SDK builds requests that cannot succeed.

I resumed the stuck agent from a copy of its JsonlLocalAgentStore in a scratch cwd, with http2.connect patched to log per-stream bytes and rstCode:

request body complete: 18.41 MiB sent
STREAM ERROR after 97ms, sent=18.41MiB recv=0B :: ERR_HTTP2_STREAM_ERROR NGHTTP2_ENHANCE_YOUR_CALM
close rstCode=11

Ten identical attempts, 396s total, ending in “Connection failed repeatedly” — matching production at 402s and 395s.

The state grew because the built-in task tool returns {conversationSteps: […]}, the subagent’s entire transcript including every tool call’s full output; 32 task calls added 9.3 MB to the parent.

useHttp1ForAgent gave no clean 50 MB BAD_REQUEST here — 18.41 MiB is under that cap, so the request simply never got a response (hung over 13 minutes).

Cap collectPreFetchedBlobs below the HTTP/2 limit rather than at 32 MiB. Treat ENHANCE_YOUR_CALM as non-retryable when the payload is unchanged, instead of ten identical re-sends. Give conversation state a compaction path, since unlike skills it cannot be pruned from disk. Surface the transport error on RunResult.

@cursor/sdk 1.0.24; Node v20.11.0; Linux 6.12.67-linuxkit x86_64; local agents (mode agent, JsonlLocalAgentStore) embedded in a long-lived Node server; model id “default”; auth via dashboard user API key.

Hey James, thanks for digging in. The evidence is great, especially the log with request body complete: 18.41 MiB / rstCode=11.

One important note: what you’re describing isn’t the same case as the original report in this thread. This thread was about the skill scanner and that part is already fixed. In your case, the limit is being hit by the conversation or checkpoint state via @cursor/sdk. The key difference is that skills can be removed from disk, but conversation state can’t, so your scenario should be handled separately.

Since this is a different root cause and a different path (SDK, not CLI), please move it to a new separate thread. This one is already closed as solved for the skills issue, and your problem will get lost here. In the new thread, please include: your @cursor/sdk version (1.0.24), Node and OS versions, and the same log with NGHTTP2_ENHANCE_YOUR_CALM / rstCode=11, plus your notes about the collectPreFetchedBlobs default (32 MiB is above the HTTP/2 limit) and the 10 identical retries of the same payload.

Not having a way to shrink conversation state (unlike skills) is something we’re already tracking. I’ll pass your details to the team. I can’t share an ETA yet.

To unblock right now, the only working option is to start a fresh conversation or agent, since the current accumulated state can’t be compressed. I get that losing context is frustrating, but there’s no way around the limit for that state at the moment.

Drop the link to the new thread here and the team will pick it up.

Moved my SDK checkpoint-state report to its own thread as you asked: SDK Run POST of 18 MiB hits ENHANCE_YOUR_CALM and bricks conversation

Same 18.41 MiB POST, rstCode=11, ten identical retries.

Please pick it up there.