Repository navigation
[Bug]: Cursor adapter always full-replays with cached_tokens=0; direct cursor-agent cache-hits on the same account #4245
Description
Client or integration
Codex CLI and Codex App (same OpenCodex proxy)
Area
Provider adapter / Cursor continuation + cache
Provider or upstream service
cursor (adapter: "cursor", OAuth, https://api2.cursor.sh)
OpenCodex version
2.48.0 (ocx --version; running proxy matched)
Operating system
macOS (darwin 25.5.0)
Endpoint or capability
POST /v1/responses → Cursor adapter runTurn / AgentService/Run
Provider and model
- OpenCodex route:
cursor/auto-intelligence(Cursor Router wire modeldefault) - Native control on the same proxy:
gpt-5.6-sol - Direct Cursor control (OpenCodex not in path):
cursor-agent --model auto
Summary
A two-turn Reply only OK / Reply only OK2 pair on the same machine and Cursor account shows:
- Direct
cursor-agent: turn 2 input collapses to 253 tokens withcacheReadTokens=25856. - Native
gpt-5.6-solthrough OpenCodex: turn 2 reportscached_input_tokens=21504. cursor/auto-intelligencethrough OpenCodex: turn 2 is 74477 input with zero cache. Provider debug iscontinuationMode=full-replay,checkpointPresent=false,checkpointInvalidationReason=missing_refon both turns.checkpoint-commit-refusedfires withemittedClientTool=true.
This is not the old large-catalog resource_exhausted failure (#190) and not the kimi-k3 short-output collapse (#1527, closed on 2.41.0). HTTP is 200. The remaining #1527 cache note (direct cacheReadTokens vs cached_tokens=0 through the proxy) still reproduces here, and the debug fields requested in #1527 are now available.
#3433 tracks intermittent zero cache on the Hermes → Chat Completions → Codex Responses path. This report is the Cursor runTurn checkpoint/replay path, so it is filed separately.
Current behaviour
Isolated Codex CLI (--ignore-user-config, openai_base_url pointed at loopback OpenCodex, no user plugins):
| Client | Model | Turn | input | cache | OpenCodex usage |
|---|---|---|---|---|---|
| Codex CLI | cursor/auto-intelligence |
1 (ephemeral) | 22122 | 0 | estimated, no cachedTokens |
| Codex CLI | gpt-5.6-sol |
1 (ephemeral) | 21159 | 0 | reported |
| Codex CLI | gpt-5.6-sol |
2 (resume) | 49654 | 21504 | reported |
| Codex CLI | cursor/auto-intelligence |
1 (persisted) | 45213 | 0 | estimated |
| Codex CLI | cursor/auto-intelligence |
2 (resume) | 74477 | 0 | estimated |
cursor-agent --print --mode ask |
auto |
1 | 18029 | 7936 | n/a (direct) |
cursor-agent resume |
auto |
2 | 253 | 25856 | n/a (direct) |
ocx debug provider on the isolated Cursor conversation (ids redacted to 12 chars):
[ocx:cursor:run-request] turnType=initial rawMessages=3 continuationMode=full-replay checkpointPresent=false checkpointInvalidationReason=missing_ref rootBlobs=4 rootBytes=54328 turnBlobs=2 tools=3
[ocx:cursor:checkpoint-commit-refused] emittedClientTool=true capturedAfterClientTool=true capturedBytes=1969
[ocx:cursor:run-request] turnType=tool-continuation rawMessages=5 continuationMode=full-replay checkpointPresent=false checkpointInvalidationReason=missing_ref rootBlobs=5 rootBytes=54394
[ocx:cursor:checkpoint-continuation] mode=full-replay
# after resume "Reply only OK2"
[ocx:cursor:run-request] action=userMessageAction rawMessages=10 continuationMode=full-replay checkpointPresent=false checkpointInvalidationReason=missing_ref rootBlobs=9 rootBytes=82007
Adapter docs say a successful no-tool turn should reuse ConversationStateStructure on the next linear continuation. These turns were no-tool user messages (Reply only OK / Reply only OK2) and still full-replayed.
Expected behaviour
On a validated linear continuation of the same Cursor account/model/thread:
continuationModeshould becomecheckpoint(or equivalent suffix-only replay) after the first successful turn, matchinghttps://opencodex.me/reference/adapters/and the fix(cursor): reuse conversation checkpoints for incremental continuation (#2054 rebased) #2277 / [Bug]: Cursor adapter large-context turns collapse or rate-limit while direct Cursor remains healthy #1527 follow-up.- OpenCodex should not grow
rootBytesfrom ~54 KiB to ~82 KiB for a one-line follow-up. - Either Cursor
cache_read/cached_tokensshould be non-zero on turn 2 (as directcursor-agentand routed Sol already are on this install), or usage should stop advertising a cache-hit counter it cannot see — but the request itself should not full-replay.
Reproduction
Proxy already running on 127.0.0.1:10100. Cursor OAuth logged in (ocx login cursor). Then:
ocx debug provider on
CODEX_BIN="$(command -v codex.opencodex-real || command -v codex)"
WORKDIR="$(mktemp -d)"
# A. isolated Cursor, two turns
"$CODEX_BIN" exec \
--ignore-user-config --skip-git-repo-check --json --color never \
-m cursor/auto-intelligence \
-c openai_base_url='"http://127.0.0.1:10100/v1"' \
-c model_catalog_json="\"${CODEX_HOME:-$HOME/.codex}/opencodex-catalog.json\"" \
-c model_reasoning_effort='"none"' \
-c approval_policy='"never"' \
-s read-only -C "$WORKDIR" \
"Reply only OK"
"$CODEX_BIN" exec resume --ignore-user-config --skip-git-repo-check --json \
--dangerously-bypass-approvals-and-sandbox \
-m cursor/auto-intelligence \
-c openai_base_url='"http://127.0.0.1:10100/v1"' \
-c model_catalog_json="\"${CODEX_HOME:-$HOME/.codex}/opencodex-catalog.json\"" \
--last \
"Reply only OK2"
# B. same pair with -m gpt-5.6-sol (control)
# C. direct Cursor, OpenCodex not in path
cursor-agent --print --mode ask --output-format json --trust \
--sandbox disabled --model auto --workspace "$WORKDIR" \
"Reply only OK"
# then --resume <session_id> "Reply only OK2"First-turn isolated Cursor vs Sol is close (22.1k vs 21.2k). The defect is the second turn.
Actual response or error
No transport error. Both Cursor turns completed 200 / OK / OK2. Codex JSON usage for the Cursor resume:
turn.completed usage: input_tokens=74477 cached_input_tokens=0 cache_write_input_tokens=0 output_tokens=24
Direct cursor-agent resume JSON (redacted ids):
usage: inputTokens=253 outputTokens=17 cacheReadTokens=25856 cacheWriteTokens=0
WebSocket to ws://127.0.0.1:10100/v1/responses returns 426 and Codex falls back to HTTP/SSE. That fallback succeeds and is not the failure.
Upstream documentation
- Cursor adapter (checkpoint reuse, no authoritative
cache_read_tokens): https://opencodex.me/reference/adapters/ - Codex integration / routed tools: https://opencodex.me/guides/codex-integration/
- Cursor is documented as an experimental adapter: https://opencodex.me/guides/providers/
No public Cursor Connect spec. The expected continuation contract is OpenCodex’s own adapter document plus the direct cursor-agent control on the same account.
Suggested mapping or implementation notes
Highest-confidence local hypothesis: resolveCursorCheckpoint() never accepts the previous turn (missing_ref), and checkpoint-commit-refused with emittedClientTool=true prevents storing a checkpoint even when the user turn had no tool call from Codex’s point of view (only the advertised exec / client-tool catalog).
#1527 asked for exactly these run-request fields. They now show full-replay on a 2-line isolated thread, not only on huge Desktop catalogs.
Please do not treat “disable Desktop App Tools” as the fix. That only slows Codex Desktop context_window_exceeded (190k window on cursor/auto-intelligence, ~35–42k/turn, cache 0). The isolated CLI pair above already has almost no plugins and still full-replays.
Additional context
Related:
- [Bug]: Cursor adapter large-context turns collapse or rate-limit while direct Cursor remains healthy #1527 (closed): large-context collapse fixed on 2.41.0; reporter’s leftover observation was
cacheReadTokensdirect vscached_tokens=0via OpenCodex. That leftover is this issue. - [Bug]: Intermittent consecutive zero cache hits for Hermes requests through OpenCodeX #3433 (open): intermittent zero cache on Hermes / Chat Completions /
session_idsynthesis. Different inbound path. - [Bug]: Cursor resource_exhausted with large Codex namespace tool catalog #190 (closed): Cursor
resource_exhaustedfrom an oversizedMcpToolscatalog. Not reproduced here (requests are 200). - [Bug]: routed first-turn tool catalog is 3–5x native Sol input tokens #1587 (closed): routed first-turn tool-catalog inflation. Isolated first turns here are already near the Sol baseline.
Codex CLI used for the pair: 0.147.0 (OpenCodex-selected). Codex Desktop 0.153.4 shows the same estimated / cache-0 pattern on cursor/auto-intelligence in daily use, but the CLI pair is the clean reproduction.
Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
- The request and response were redacted.
- The expected behaviour is based on OpenCodex adapter docs and a same-account direct Cursor control.
Activity
lidge-jun commentedon Sep 11, 2026
리뷰 · 우선순위 75 / 80
설명
이 이슈는 Cursor 어댑터로 두 턴만 이어서 말해도 매번
continuationMode=full-replay에cached_tokens=0이 나오는 버그입니다. 같은 맥·같은 Cursor 계정에서 직접cursor-agent를 쓰면 두 번째 턴이 입력 253토큰에cacheReadTokens=25856이고, OpenCodex로 같은 프록시를 통과한 네이티브gpt-5.6-sol도 두 번째 턴에cached_input_tokens=21504가 잡힙니다. 그런데 라우트cursor/auto-intelligence(와이어 모델default)만 두 번째 턴 입력이 74477로 커지고 캐시는 0입니다. HTTP는 200이고, 예전 큰 카탈로그resource_exhausted(#190)나 kimi 짧은 출력(#1527 본편)이 아닙니다. #1527이 닫힌 뒤 남았던 「직접 Cursor는 캐시 히트 / 프록시는 cached_tokens=0」 잔여 관찰이 그대로 재현된 보고서입니다.지금
dev(HEADbabb76449, package.json 2.51.0) 코드를 이 로그에 대어 보면 원인 후보가 거의 한 줄로 모입니다.src/adapters/cursor/request-builder.ts의resolveCursorCheckpoint()는 이전 턴 체크포인트가 없으면missing_ref를 내고 요청을full-replay로 만듭니다. 보고자의ocx debug provider로그가 정확히 그 쌍입니다(checkpointPresent=false,checkpointInvalidationReason=missing_ref). 그런데 체크포인트가 없는 이유는 「저장을 안 해서」에 가깝습니다.src/adapters/cursor.ts의commitCapturedCheckpoint()는 클라이언트 도구가 한 번이라도 나가면(emittedClientTool=true)isCursorExternalWireModel(modelId)가 참일 때만 도구 중단 체크포인트를 커밋합니다.auto-intelligence→ 와이어default는src/adapters/cursor/discovery.ts에서 네이티브/라우터로 분류되므로isCursorExternalWireModel이 거짓입니다. 그래서 로그의checkpoint-commit-refused emittedClientTool=true capturedAfterClientTool=true가 나오고, 다음 턴은 다시missing_ref→ 전체 재전송 → 캐시 0이 됩니다. Codex 입장에서는 「도구 없는 한 줄 대답」이어도, Cursor 쪽이tool_call_end를 한 번이라도 내면 이 가드에 걸립니다.보고자가 기대하는 동작은 어댑터 문서와 #2277 이후 설계와 같습니다. 검증된 같은 계정·모델·스레드의 선형 이어말하기에서는
continuationMode=checkpoint(또는 그에 가까운 접미사만 보내는 경로)로 가야 하고, 한 줄 후속 질문에rootBytes가 54KiB → 82KiB로 불어나면 안 됩니다. 캐시 카운터를 못 읽더라도 요청 자체가 full-replay면 안 된다는 점이 핵심입니다. 「Desktop App Tools 끄기」는 고치라는 처방이 아닙니다. 격리 CLI 재현에도 이미 거의 플러그인이 없는데 full-replay가 납니다.현재
dev방향(방금 랜딩한 L4 #4240 클라이언트 카탈로그 호환, L7 원격 허브, L6 Qoder, L3 풀 정책)과는 레인이 다릅니다. 이건 Cursor Connect 체크포인트/이어말하기 안정성 버그입니다. 열려 있는 #4229(첫 overflow surface 후 remint)와도 실패 종류가 다릅니다. #4229는 컨텍스트 한계 overflow 복구이고, 이 이슈는 성공한 200 턴에서 체크포인트를 못 심어 매 턴 full-replay하는 문제입니다. #3433은 Hermes → Chat Completions → Responses 경로의 간헐 zero-cache라 인바운드가 다릅니다. 중복으로 닫지 마세요. types.ts/config.ts 스플릿과도 무관합니다.우선순위 75인 이유: 재현이 짧고, 보고자가 이미 #1527이 요청했던
run-request진단 필드를 붙여 줬으며, HEAD 코드의missing_ref/ 네이티브 모델 커밋 거절 가드와 로그가 맞습니다. Cursor 실사용에서 토큰·지연·컨텍스트 한도에 바로 닿는 버그입니다. 다만 보고 버전은 2.48.0이고 팁은 2.51.0이라, 패치 전에 같은 격리 스크립트를 팁에서 한 번 더 돌려 회귀가 그대로인지 확인하는 게 좋습니다. 첫 패치는 「캐시 숫자를 꾸며 넣기」가 아니라 네이티브/라우터 모델에서도 안전한 체크포인트 커밋·재사용을 복구하는 쪽이어야 합니다.라인 190-212 -
src/adapters/cursor.tscommitCapturedCheckpoint: 네이티브 모델에서emittedClientTool=true면toolSuspendedCommit이 거짓이 되어 커밋 거절. 보고 로그와 동일라인 198 -
isCursorExternalWireModel이 거짓인default/auto-intelligence는 도구 중단 체크포인트 허용 밖. 외부 모델 전용 가드가 네이티브 라우터를 막아 버림라인 438-459 -
resolveCursorCheckpoint가 ref/스냅샷 없으면missing_ref→full-replay. 커밋이 거절된 다음 턴의 직접 결과경로
src/adapters/cursor/discovery.tsisCursorNativeWireModel-default/auto/composer-*는 네이티브. 그래서 external-only 커밋 경로에 안 들어감경로
src/adapters/cursor/checkpoint-store.ts- TTL 15분·엔트리 64·16MiB 상한은 정상이고, 이번 재현의 1차 원인은 「저장 실패」이지 「만료」가 아님(expired가 아니라missing_ref)경로 이슈 구분 - #3433(Hermes Chat Completions)·#4229(overflow remint)·#190(카탈로그 overflow)과 묶지 말 것. #1527 잔여 캐시 관찰의 Cursor runTurn 후속
메인테이너의 판단이 필요한 지점
default, composer)에도 도구 중단 체크포인트 커밋을 열지, 아니면 「도구가 나간 턴」과 「순수 사용자 이어말하기」를 더 잘게 나눠 순수 턴만 커밋할지tool_call_end를 내는 경우를 emittedClientTool에서 빼도 안전한지cached_tokens를 Cursorcache_read에 맞출지, 우선은 continuationMode만 checkpoint로 고치고 카운터는 estimated로 둘지너의 추천
열어 두고 Cursor 체크포인트 버그로 추적하세요. 중복 닫기 금지(#3433/#4229와 다름). 다음 단계는 팁(2.51.0 /
babb76449)에서 보고자 격리 스크립트를 한 번 재현한 뒤,commitCapturedCheckpoint가 네이티브default에서도 선형 no-op 이어말하기 체크포인트를 남기게 고치는 PR입니다. 고친 뒤에는 같은 두 턴에서continuationMode=checkpoint와 rootBytes 증가 없음이 회귀 기준입니다. 캐시 카운터 표시는 그다음입니다.이 댓글은 grok-bot이 작성했습니다
lidge-jun commentedon Sep 11, 2026
Live probe on a real Cursor account — the external/native split is not the cause
Instrumented on macOS, opencodex 2.50.0, a Cursor OAuth account logged in through
ocx, provider debug toggled at runtime (ocx debug provider on). Requests went to the local proxy at/v1/chat/completions; nothing was patched.1. Every model fails at
capturedBytes: 0, not at the model gateSame forced-tool-call request, three model classes:
externalModel: truefails identically toexternalModel: false. The tool-suspended commit path incommitCapturedCheckpointis therefore dead for every model, not just native ones —capturedAfterClientToolisfalseeverywhere, sotoolSuspendedCommitcan never be true, and the guard falls through tocapturedBytes === 0regardless.This matters for anyone proposing a fix: relaxing
isCursorExternalWireModelin that guard changes nothing. The very next condition —!lastTransport?.captured || lastTransport.captured.byteLength === 0— still refuses. A patch there would add risk and fix zero cases.2. Capture works fine on a turn that emits no client tool
So
conversationCheckpointUpdatedoes arrive andcapturedCheckpointBytesis set — just not on a turn that suspends for a client tool. On that path the frame has not arrived by the timeclient-tool-suspendfires and callscancelCursorRun():No
conversationCheckpointUpdateappears among those 33 frames. Open question I could not settle without a code change: does Cursor send it later on a suspended tool turn (so the finalize grace is simply too short), or never (so the state is only serialized for a turn upstream considers complete)?clientToolFinalizeGraceMsForRequestis an internal constant with no config key, so testing that needs a build.3. A second, independent cause on the stateless path
Two sequential turns, same conversation content:
The
conversationIddiffers between turns, so turn 2 cannot find the checkpoint turn 1 successfully committed.cached_tokenswas0on both. This is themissing_refin the original report, and on this path it is a conversation-identity problem rather than a checkpoint-capture one.Honest scope limit: this was observed on
/v1/chat/completions, which carries no Responses state. Whether the/v1/responsespath keeps a stableconversationIdacross turns needs the same probe before anyone treats (3) as general.Consequence
#4245is at least two defects wearing one symptom, and neither is the native/external split. Recording this so the next attempt starts from the capture path and the conversation identity, not from the model gate.lidge-jun commentedon Sep 11, 2026
Fixed on
devby #4281The capture half of this issue is fixed. Summary of what the investigation actually found, since it is not what the issue was first triaged with.
The native/external model gate was never the cause. On a client-tool turn the refusal is over-determined:
checkpoint-commit-refusedreportscapturedBytes: 0for the native router, native composer and external Claude alike, so theisCursorExternalWireModeltest one line above it is unreachable. Relaxing it would have changed nothing.The cause was a 50 ms window. A client-tool turn suspends before
turnEnded, and upstream sends that turn'sconversationCheckpointUpdateright aftertoolCallStarted— afterCLIENT_TOOL_FINALIZE_GRACE_MS, socancelCursorRun()killed the stream first. Isolated by holding the tool catalog constant on the wire and varying only the local grace (parallel_tool_callsis read locally and never protobuf-encoded):toolCallStartedcapturedBytesReproduced in both directions in one session. #4281 gives a drained client-tool turn one bounded extension and fires early the moment the frame lands, so a turn that receives a checkpoint pays arrival latency (256 ms in the suite) rather than the whole window (1816 ms when none arrives).
The conversation-identity half is closed without a patch. Two
/v1/responsesturns threadingprevious_response_idshareconversationHash cursor_cdbed7dcc, and turn 2 reportsmode: checkpointrather thanfull-replay— the continuation genuinely resumes. The fresh id per turn only happens on/v1/chat/completions, which carries no Responses state, and on an unthreaded one-shot Responses call where there is no prior conversation to resume.What is still open
Native wire models still refuse the tool-suspended commit, so this does not yet restore caching for
auto/composer-*.capturedAfterClientToolis set from arrival order, not snapshot contents, so it proves the bytes arrived after the tool call and not that they cover it. Moving that gate requires decoding the capturedConversationStateStructurefirst. Deliberately not guessed.Full record, including three wrong hypotheses and the 500-line log-ring artifact that briefly made a correct result look reversed:
devlog/_plan/260911_cursor_checkpoint_capture/.Leaving this issue open for the native half. Reopen scope if
cached_tokensstays 0 for external models after the next release.lidge-jun commentedon Sep 11, 2026
Landed via #4281 at
1acff74944dde72dca72f6e1f29c649c3ba9438d.Cursor live-transport now extends finalize grace so a suspended client-tool turn can capture
conversationCheckpointUpdate(the real cause ofcached_tokens: 0full-replays). Ondevas of that merge.이 댓글은 grok-bot이 작성했습니다
lidge-jun commentedon Sep 11, 2026
Closed: fix is on
devvia #4281 (1acff74944dde72dca72f6e1f29c649c3ba9438d).docs(devlog): open the cursor checkpoint capture unit for lidge-jun#4245
feat(cursor): report decoded checkpoint shape so coverage is answerab…
docs(devlog): open the cursor checkpoint capture unit for lidge-jun#4245
feat(cursor): report decoded checkpoint shape so coverage is answerab…