Skip to content

[Bug]: Cursor adapter always full-replays with cached_tokens=0; direct cursor-agent cache-hits on the same account #4245

Closed

Description

@whelp99-code

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 model default)
  • 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 with cacheReadTokens=25856.
  • Native gpt-5.6-sol through OpenCodex: turn 2 reports cached_input_tokens=21504.
  • cursor/auto-intelligence through OpenCodex: turn 2 is 74477 input with zero cache. Provider debug is continuationMode=full-replay, checkpointPresent=false, checkpointInvalidationReason=missing_ref on both turns. checkpoint-commit-refused fires with emittedClientTool=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:

  1. continuationMode should become checkpoint (or equivalent suffix-only replay) after the first successful turn, matching https://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.
  2. OpenCodex should not grow rootBytes from ~54 KiB to ~82 KiB for a one-line follow-up.
  3. Either Cursor cache_read / cached_tokens should be non-zero on turn 2 (as direct cursor-agent and 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

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:

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

  1. added
    account-poolOAuth, credentials, Codex pool, quota, failover, plans
    catalogModel catalog, slugs, visibility, routed entries
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    on Sep 11, 2026
  2. lidge-jun commented on Sep 11, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 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(HEAD babb76449, 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.ts commitCapturedCheckpoint: 네이티브 모델에서 emittedClientTool=true면 toolSuspendedCommit이 거짓이 되어 커밋 거절. 보고 로그와 동일
    라인 198 - isCursorExternalWireModel이 거짓인 default/auto-intelligence는 도구 중단 체크포인트 허용 밖. 외부 모델 전용 가드가 네이티브 라우터를 막아 버림
    라인 438-459 - resolveCursorCheckpoint가 ref/스냅샷 없으면 missing_ref → full-replay. 커밋이 거절된 다음 턴의 직접 결과
    경로 src/adapters/cursor/discovery.ts isCursorNativeWireModel - 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)에도 도구 중단 체크포인트 커밋을 열지, 아니면 「도구가 나간 턴」과 「순수 사용자 이어말하기」를 더 잘게 나눠 순수 턴만 커밋할지
    • Codex가 광고한 클라이언트 도구 카탈로그만으로 Cursor가 tool_call_end를 내는 경우를 emittedClientTool에서 빼도 안전한지
    • usage의 cached_tokens를 Cursor cache_read에 맞출지, 우선은 continuationMode만 checkpoint로 고치고 카운터는 estimated로 둘지
    • L4/L7 레인 기차와 별도 Cursor 안정성 핫픽스로 먼저 넣을지

    너의 추천
    열어 두고 Cursor 체크포인트 버그로 추적하세요. 중복 닫기 금지(#3433/#4229와 다름). 다음 단계는 팁(2.51.0 / babb76449)에서 보고자 격리 스크립트를 한 번 재현한 뒤, commitCapturedCheckpoint가 네이티브 default에서도 선형 no-op 이어말하기 체크포인트를 남기게 고치는 PR입니다. 고친 뒤에는 같은 두 턴에서 continuationMode=checkpoint와 rootBytes 증가 없음이 회귀 기준입니다. 캐시 카운터 표시는 그다음입니다.

    이 댓글은 grok-bot이 작성했습니다

  3. lidge-jun commented on Sep 11, 2026

    @lidge-jun
    Owner

    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 gate

    Same forced-tool-call request, three model classes:

    cursor/auto-intelligence  (native router)
    [ocx:cursor:checkpoint-commit-refused] {"replayUnsafe":false,"emittedClientTool":true,"capturedAfterClientTool":false,"externalModel":false,"storeCheckpoints":true,"capturedBytes":0}
    
    cursor/claude-4.5-sonnet  (external)
    [ocx:cursor:checkpoint-commit-refused] {"replayUnsafe":false,"emittedClientTool":true,"capturedAfterClientTool":false,"externalModel":true,"storeCheckpoints":true,"capturedBytes":0}
    
    cursor/composer-2.5-fast  (native composer)
    [ocx:cursor:checkpoint-commit-refused] {"replayUnsafe":false,"emittedClientTool":true,"capturedAfterClientTool":false,"externalModel":false,"storeCheckpoints":true,"capturedBytes":0}
    

    externalModel: true fails identically to externalModel: false. The tool-suspended commit path in commitCapturedCheckpoint is therefore dead for every model, not just native ones — capturedAfterClientTool is false everywhere, so toolSuspendedCommit can never be true, and the guard falls through to capturedBytes === 0 regardless.

    This matters for anyone proposing a fix: relaxing isCursorExternalWireModel in 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

    [ocx:cursor:checkpoint-continuation] {"mode":"full-replay","checkpointRefHash":"c5609327a9ac1ec4","checkpointBytes":492,"wireModel":"default"}
    [ocx:cursor:checkpoint-continuation] {"mode":"full-replay","checkpointRefHash":"bdd0d48f85f7ebee","checkpointBytes":553,"wireModel":"default"}
    

    So conversationCheckpointUpdate does arrive and capturedCheckpointBytes is set — just not on a turn that suspends for a client tool. On that path the frame has not arrived by the time client-tool-suspend fires and calls cancelCursorRun():

    [ocx:cursor:client-tool-suspend] {"reason":"Responses bridge owns client tools; ending turn without fake mcpResult","framesReceived":33,"elapsedMs":2886}
    [ocx:cursor:stream-cancel-expected] {"code":"ERR_HTTP2_STREAM_ERROR","message":"Cursor stream suspended: Stream closed with error code NGHTTP2_CANCEL"}
    

    No conversationCheckpointUpdate appears 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)? clientToolFinalizeGraceMsForRequest is 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:

    [ocx:cursor:run-request] {"conversationId":"cursor_f3e3e375188f41b9af0669d7090eb962","continuationMode":"full-replay","checkpointPresent":false,"checkpointInvalidationReason":"missing_ref"}
    [ocx:cursor:run-request] {"conversationId":"cursor_24bbb91416874b53b9a87f97530cfa14","continuationMode":"full-replay","checkpointPresent":false,"checkpointInvalidationReason":"missing_ref"}
    

    The conversationId differs between turns, so turn 2 cannot find the checkpoint turn 1 successfully committed. cached_tokens was 0 on both. This is the missing_ref in 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/responses path keeps a stable conversationId across turns needs the same probe before anyone treats (3) as general.

    Consequence

    #4245 is 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.

  4. lidge-jun commented on Sep 11, 2026

    @lidge-jun
    Owner

    Fixed on dev by #4281

    The 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-refused reports capturedBytes: 0 for the native router, native composer and external Claude alike, so the isCursorExternalWireModel test 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's conversationCheckpointUpdate right after toolCallStarted — after CLIENT_TOOL_FINALIZE_GRACE_MS, so cancelCursorRun() killed the stream first. Isolated by holding the tool catalog constant on the wire and varying only the local grace (parallel_tool_calls is read locally and never protobuf-encoded):

    Local grace Checkpoint after toolCallStarted capturedBytes
    50 ms no 0
    1500 ms yes 3036

    Reproduced 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/responses turns threading previous_response_id share conversationHash cursor_cdbed7dcc, and turn 2 reports mode: checkpoint rather than full-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-*. capturedAfterClientTool is 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 captured ConversationStateStructure first. 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_tokens stays 0 for external models after the next release.

  5. lidge-jun commented on Sep 11, 2026

    @lidge-jun
    Owner

    Landed via #4281 at 1acff74944dde72dca72f6e1f29c649c3ba9438d.

    Cursor live-transport now extends finalize grace so a suspended client-tool turn can capture conversationCheckpointUpdate (the real cause of cached_tokens: 0 full-replays). On dev as of that merge.

    이 댓글은 grok-bot이 작성했습니다

  6. lidge-jun commented on Sep 11, 2026

    @lidge-jun
    Owner

    Closed: fix is on dev via #4281 (1acff74944dde72dca72f6e1f29c649c3ba9438d).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, planscatalogModel catalog, slugs, visibility, routed entrieslanded-via-maintainerOriginal PR closed after landing via a maintainer merge trainproviderProvider adapters, OpenAI-compat presets, upstream API quirksprovider-compatibilityProvider compatibility reports

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions