Skip to content

fix(cursor): fail broken Cursor turns and keep permanent errors after turn end - #425

Merged
shuv1337 merged 4 commits into
mainfrom
fix/cursor-sdk-1.3.1-release-staging
Nov 9, 2026
Merged

shuv1337 merged 4 commits into
mainfrom
fix/cursor-sdk-1.3.1-release-staging

Conversation

@shuv1337

@shuv1337 shuv1337 commented Nov 9, 2026

Copy link
Copy Markdown
Owner

Base: main (branched from 8d639a339662c67b794486acc9a5a0ec713eeb34; merges cleanly with current main 25e868c3d1383ba23e3910e4b6e0e67dc8b614e7, which adds only #424's one-line ci-full.yml timeout change) · Head: fix/cursor-sdk-1.3.1-release-staging (88f29f5dac8b8da11fc7952af174f7b73127d72b)

Problem

The #422 post-merge review returned HOLD (medium) with two Cursor state-correctness findings:

  1. Medium: finalizeTurn in the Cursor adapter always reported the provider thread as active, even when the turn ended with a broken thread disposition (an unconfirmed checkpoint). The session manager unloads a runtime only when it sees status: "error", so the next message in the same runtime reused the uncertain agent instead of starting fresh.
  2. Low: the @cursor/sdk@1.3.1 patch's retry guard (dist/s2code-retry.cjs) replaced every classifier decision after turnEnded, other than cancellation, with the checkpoint-uncertain error. Permanent failures such as AuthenticationError lost their kind, message and diagnostics.

#423 already shipped the apps/server @cursor/sdk 1.3.1 pin and its lockfile refresh, the cause of the desktop-staging failure in Fork release run 42321780105. This PR does not repeat that change.

Fix

  • packages/provider-cursor/src/server/adapter.ts: finalizeTurn now sends status: threadDisposition === "broken" ? "error" : "active", as the OpenCode adapter does, so the manager drops the loaded runtime.
  • patches/@cursor__sdk@1.3.1.patch: the guard now blocks only retry decisions: blocked = turnEnded && decision.action === "retry" && checkpoint !== "ineligible_terminal_turn". throw decisions, including auth, permanent and cancelled errors, pass through unchanged with their original diagnostics. The patch's hunk length and index hash were corrected.
  • pnpm-lock.yaml: only the @cursor/sdk@1.3.1 patch hash changes (9539e59b… to 55ea990e…). No specifier or version changes.
  • scripts/build-desktop-artifact.test.ts: a broader check runs every workspace manifest's dependencies, optionalDependencies and devDependencies (after resolving catalog: specs) through the same pin-and-match logic that desktop and CLI staging use. A spec that drifts from a patched version now fails in the scripts tests instead of at release time. This sits alongside fix(release): align packaged Cursor SDK with retry patch #423's narrower server-equals-provider assertion.

Regression tests

  • (a) adapter.test.ts "ends the turn with $reason": the last provider_thread.updated is error for a broken disposition and active otherwise.
  • (b) OrchestratorReplayRecovery.integration.test.ts "hands the next Cursor turn to a fresh agent after an unconfirmed checkpoint": both turns run in the same runtime, and the second turn must open a new agent (agent-after-checkpoint-failure) with an inline Context handoff (full_thread_summary).
  • (c) cursorRetry.test.ts "authentication failure after an answer", for both cjs and esm: the error keeps its original message and kind: AuthenticationError, and the diagnostic records throw with the original cause.
  • (d) cursorRetry.test.ts "turns only retries into checkpoint failures": after turnEnded, retry becomes the checkpoint error, and throw decisions with AuthenticationError, CancelledError or NonRetriableError come back as the identical decision object.

Verification (local, final SHA 88f29f5dac8b8da11fc7952af174f7b73127d72b)

Check Result
vp run check:changed pass: unused patches, format, lint, knip 1 and 2, graphql, typecheck of 41 packages, 123 tests in 5 files
packages/provider-cursor full suite 16 files, 151 passed
apps/server orchestration-v2 Cursor suites (21 files) 957 passed
scripts/build-desktop-artifact.test.ts 70 passed

Negative controls. Each test fails without its fix and passes with it:

Gate Without the fix With the fix
(a) fails: expected 'active' to equal 'error' 9 passed
(b) fails: turn 2 "The provider could not start this turn" 1 passed
(c) 2 fail (cjs and esm): error rewritten to the checkpoint message 2 passed
(d) the throw AuthenticationError and throw NonRetriableError rows fail 4 passed
all-manifest check (server set to 1.2.0) fails: apps/server/package.json dependencies: @cursor/sdk spec 1.2.0 does not match patched versions 1.3.1 70 passed

For (a) and (b), "without the fix" means reversing only the finalizeTurn change. For (c) and (d), it means swapping the base guard file into node_modules.

Staging proof (macOS arm64, built from 88f29f5dac8b8da11fc7952af174f7b73127d72b)

The steps follow release-desktop.yml, with Node 26.12.1, previous outputs deleted, --no-cache on the bundle and desktop steps, and local placeholder version 0.1.7-nightly.20261107.913:

  1. update-release-package-versions.ts, then restore afterwards
  2. vp run --no-cache build:desktop
  3. cargo build --release --target aarch64-apple-darwin
  4. web branding
  5. cli.ts build-exe
  6. build-cli-archive.ts
  7. smoke-cli-archive.ts
  8. vp run --no-cache dist:desktop:artifact --platform mac --target dmg --arch arm64 --skip-build --verbose --keep-stage
Artifact Bytes sha256
/tmp/s2stage/final/release-cli/s2code-0.1.7-nightly.20261107.913-darwin-arm64.tar.gz 142,941,315 03ea940ec57af0f299c20a11e9a03c1a6cc2672398a4fb5daf6b362c5909cc93
release/S2Code-0.1.7-nightly.20261107.913-arm64.dmg 222,560,344 033a08e18fd4c1557e5cc9f3ff15e561a2af8a7a1991211e92e0afe9a5decefc
release/S2Code-0.1.7-nightly.20261107.913-arm64.zip 214,348,691 b1d7e62d26396ddc3f0691503873a1261e4ace27e0bb6a1e2041a40ed9ec03be
  • CLI archive:
    • The smoke test passed: --version, __service-launcher, and serve answered on port 47700.
    • Contains @cursor/sdk 1.3.1 and @cursor/sdk-darwin-arm64 1.3.1. dist/s2code-retry.cjs has const blocked = turnEnded && decision.action === "retry" && checkpoint !== "ineligible_terminal_turn";, and both dist/cjs/305.js and dist/esm/305.js call s2codeRetryGuard.guard.
  • Desktop:
    • The macOS payload passed validation, and electron-builder reported isPublish: false.
    • The stage installed @cursor+sdk@1.3.1_patch_hash=55ea990e….
    • app.asar contains @cursor/sdk 1.3.1 with the same guard line, called from both the cjs and esm 305.js.
    • The app package.json is version 0.1.7-nightly.20261107.913 and declares @cursor/sdk 1.3.1. The unpacked @cursor/sdk-darwin-arm64 is 1.3.1.

Logs (under /tmp/s2stage/final/):

  • Checks: check-changed.log, provider-cursor-suite.log, server-cursor-suites-fg.log, build-desktop-artifact-test.log
  • Controls: gate-{a,b,c,d}-{with,without}-fix.log, manifest-without-fix.log
  • Staging: stamp.log, build-desktop.log, cargo.log, brand.log, build-exe.log, cli-archive.log, smoke-cli.log, desktop.log
  • staged-sha.txt records the staged SHA.

Limits

  • The local proof covers macOS arm64 only, unsigned. The CLI was signed ad hoc.
  • There is no Linux, Windows, real SDK transport, or real classifier proof, and no bundled-loop proof.
  • The bundled SDK entrypoint loop (dist/bundled/index.js, used only under the bun export condition) is dormant and unpatched. The current Node CJS/ESM load path (require to dist/cjs, import to dist/esm) is covered.

Pending gates

None of these has completed yet:

  • A pinned Opus re-review of the final SHA (separate session)
  • CI (runs automatically on this PR)
  • CI Full (dispatched once on this branch)

shuv1337 and others added 4 commits November 6, 2026 17:45
…e patch

Run each workspace manifest's dependencies through the same pin-and-match
check desktop and CLI staging use, so a spec that drifts from a patched
version (as @cursor/sdk 1.2.0 did against the 1.3.1 patch) fails in the
scripts tests instead of at release time.

Claude Opus 5.5 via Cursor in T3 Code
finalizeTurn always reported the provider thread as active, so after an
unconfirmed checkpoint the session manager kept the loaded runtime and the next
turn reused the uncertain agent. Report error for a broken disposition, as
OpenCode does, so the next turn opens a fresh agent with a context handoff.

Claude Opus 5.5 via Cursor in T3 Code

Co-authored-by: Cursor <cursoragent@cursor.com>
The 1.3.1 retry guard replaced every non-cancelled classifier decision after
turnEnded with the checkpoint-uncertain error, so authentication and other
permanent failures lost their kind and message. Only block retries; throw
decisions now pass through with their original error and diagnostics.

Claude Opus 5.5 via Cursor in T3 Code

Co-authored-by: Cursor <cursoragent@cursor.com>
Assert that finalizeTurn reports error for a broken disposition and active
otherwise, and that after turnEnded the SDK retry guard rewrites only retry
decisions; every throw keeps its original error and kind.

Claude Opus 5.5 via Cursor in T3 Code

Co-authored-by: Cursor <cursoragent@cursor.com>
@shuv1337
shuv1337 merged commit 9c16b74 into main Nov 9, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant