cursor-opencode-provider 0.8.0 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/README.md +230 -44
  3. package/SECURITY.md +60 -12
  4. package/dist/activity.d.ts +73 -1
  5. package/dist/activity.js +231 -5
  6. package/dist/agent-url.js +37 -9
  7. package/dist/auth-renewal.d.ts +1 -1
  8. package/dist/auth-renewal.js +58 -26
  9. package/dist/billing.d.ts +105 -0
  10. package/dist/billing.js +212 -0
  11. package/dist/classic-tool-results.d.ts +1 -0
  12. package/dist/classic-tool-results.js +42 -0
  13. package/dist/context/build.d.ts +23 -4
  14. package/dist/context/build.js +65 -10
  15. package/dist/context/dynamic-catalog.js +6 -1
  16. package/dist/context/env.js +27 -3
  17. package/dist/context/epoch.js +9 -5
  18. package/dist/context/frozen.js +6 -2
  19. package/dist/context/git.d.ts +5 -2
  20. package/dist/context/git.js +17 -7
  21. package/dist/context/index.d.ts +2 -0
  22. package/dist/context/index.js +2 -0
  23. package/dist/context/paths.d.ts +45 -18
  24. package/dist/context/paths.js +72 -22
  25. package/dist/context/skills-bridge.d.ts +22 -0
  26. package/dist/context/skills-bridge.js +20 -0
  27. package/dist/context/skills.d.ts +34 -0
  28. package/dist/context/skills.js +211 -0
  29. package/dist/debug.js +13 -2
  30. package/dist/early-tool-rows.d.ts +60 -0
  31. package/dist/early-tool-rows.js +96 -0
  32. package/dist/errors.d.ts +49 -7
  33. package/dist/errors.js +163 -8
  34. package/dist/host-agent-mode.d.ts +53 -4
  35. package/dist/host-agent-mode.js +117 -13
  36. package/dist/host-interrupt.d.ts +12 -0
  37. package/dist/host-interrupt.js +18 -0
  38. package/dist/host-plan-file.d.ts +28 -0
  39. package/dist/host-plan-file.js +76 -0
  40. package/dist/host-steer.d.ts +71 -0
  41. package/dist/host-steer.js +232 -0
  42. package/dist/image-input.d.ts +18 -0
  43. package/dist/image-input.js +122 -30
  44. package/dist/image-save.d.ts +10 -2
  45. package/dist/image-save.js +33 -21
  46. package/dist/index.d.ts +2 -2
  47. package/dist/language-model.d.ts +149 -30
  48. package/dist/language-model.js +3127 -673
  49. package/dist/lost-request.d.ts +39 -0
  50. package/dist/lost-request.js +111 -0
  51. package/dist/model-config.d.ts +12 -1
  52. package/dist/model-config.js +78 -30
  53. package/dist/models.d.ts +26 -3
  54. package/dist/models.js +101 -45
  55. package/dist/opencode2/catalog.d.ts +1 -0
  56. package/dist/opencode2/catalog.js +2 -0
  57. package/dist/opencode2/image-save-tool.d.ts +8 -0
  58. package/dist/opencode2/image-save-tool.js +84 -0
  59. package/dist/opencode2/tool-results.d.ts +18 -0
  60. package/dist/opencode2/tool-results.js +66 -0
  61. package/dist/opencode2/types.d.ts +15 -0
  62. package/dist/parallel-step.d.ts +65 -0
  63. package/dist/parallel-step.js +102 -0
  64. package/dist/plugin-opencode2.js +233 -65
  65. package/dist/plugin.js +97 -23
  66. package/dist/pricing-data.d.ts +40 -3
  67. package/dist/pricing-data.js +40 -3
  68. package/dist/pricing.d.ts +1 -1
  69. package/dist/pricing.js +7 -4
  70. package/dist/process-shared.d.ts +14 -0
  71. package/dist/process-shared.js +18 -0
  72. package/dist/protocol/apply-patch.d.ts +25 -13
  73. package/dist/protocol/apply-patch.js +79 -30
  74. package/dist/protocol/ask-question.d.ts +12 -1
  75. package/dist/protocol/ask-question.js +77 -3
  76. package/dist/protocol/blob-store.d.ts +5 -1
  77. package/dist/protocol/blob-store.js +10 -6
  78. package/dist/protocol/client-version.js +6 -4
  79. package/dist/protocol/conversation-cache-header.d.ts +23 -0
  80. package/dist/protocol/conversation-cache-header.js +149 -0
  81. package/dist/protocol/conversation-persistence.d.ts +27 -1
  82. package/dist/protocol/conversation-persistence.js +628 -126
  83. package/dist/protocol/conversation-state.d.ts +8 -1
  84. package/dist/protocol/conversation-state.js +28 -7
  85. package/dist/protocol/create-plan.d.ts +47 -25
  86. package/dist/protocol/create-plan.js +33 -23
  87. package/dist/protocol/host-notes.d.ts +31 -0
  88. package/dist/protocol/host-notes.js +85 -0
  89. package/dist/protocol/interactions.d.ts +26 -17
  90. package/dist/protocol/interactions.js +80 -31
  91. package/dist/protocol/messages.js +164 -21
  92. package/dist/protocol/request.d.ts +40 -19
  93. package/dist/protocol/request.js +55 -28
  94. package/dist/protocol/sent-history-images.d.ts +49 -0
  95. package/dist/protocol/sent-history-images.js +102 -0
  96. package/dist/protocol/switch-mode.d.ts +66 -27
  97. package/dist/protocol/switch-mode.js +147 -73
  98. package/dist/protocol/token-details.d.ts +2 -0
  99. package/dist/protocol/token-details.js +11 -1
  100. package/dist/protocol/tool-call-bridge.d.ts +6 -0
  101. package/dist/protocol/tool-call-bridge.js +24 -0
  102. package/dist/protocol/tools.d.ts +90 -9
  103. package/dist/protocol/tools.js +543 -151
  104. package/dist/protocol/turn-provenance.d.ts +19 -4
  105. package/dist/protocol/turn-provenance.js +77 -20
  106. package/dist/replay-safety.d.ts +22 -4
  107. package/dist/replay-safety.js +86 -27
  108. package/dist/retry-budget.d.ts +21 -0
  109. package/dist/retry-budget.js +94 -0
  110. package/dist/runtime-files.d.ts +23 -0
  111. package/dist/runtime-files.js +581 -0
  112. package/dist/session.d.ts +129 -10
  113. package/dist/session.js +198 -35
  114. package/dist/shared.d.ts +1 -1
  115. package/dist/shared.js +1 -1
  116. package/dist/shell-timeout.d.ts +54 -10
  117. package/dist/shell-timeout.js +362 -92
  118. package/dist/stream-write.d.ts +20 -0
  119. package/dist/stream-write.js +87 -0
  120. package/dist/terminal-runner.d.ts +15 -0
  121. package/dist/terminal-runner.js +528 -0
  122. package/dist/todo-store.d.ts +2 -0
  123. package/dist/todo-store.js +19 -0
  124. package/dist/transport/connect.d.ts +10 -1
  125. package/dist/transport/connect.js +124 -27
  126. package/dist/usage.d.ts +37 -17
  127. package/dist/usage.js +85 -34
  128. package/package.json +5 -3
  129. package/dist/plan-execution-kickoff.d.ts +0 -57
  130. package/dist/plan-execution-kickoff.js +0 -186
  131. package/dist/protocol/thinking.d.ts +0 -15
  132. package/dist/protocol/thinking.js +0 -17
package/README.md CHANGED
@@ -4,7 +4,7 @@ Use [Cursor](https://cursor.com) subscription models from [OpenCode](https://ope
4
4
 
5
5
  This project is a custom **AI SDK provider** (`LanguageModelV3`) plus an **OpenCode plugin** that handles authentication and model discovery. Instead of calling a generic chat-completions API, it encodes and decodes Cursor's protobuf agent protocol over HTTP/2 to Cursor's agent backend.
6
6
 
7
- > **Runs unchanged across OpenCode, OpenCode 2.0, and four additional coding agents.** OpenCode loads this plugin natively, including through the dedicated OpenCode 2.0 entrypoint. [OCP — OpenCode Plugin Compatibility](https://github.com/oakimov/opencode-plugin-compat) runs the same published plugin, without a provider fork, in **Kilo Code, MiMo Code, pi, and oh-my-pi**. See [Coding-agent compatibility](#coding-agent-compatibility).
7
+ > **Runs unchanged across OpenCode, OpenCode 2.0, and five additional coding agents.** OpenCode loads this plugin natively, including through the dedicated OpenCode 2.0 entrypoint. [OCP — OpenCode Plugin Compatibility](https://github.com/oakimov/opencode-plugin-compat) runs the same published plugin, without a provider fork, in **Kilo Code, MiMo Code, pi, oh-my-pi, and DeepSeek Harness**. See [Coding-agent compatibility](#coding-agent-compatibility).
8
8
  >
9
9
  > **Status:** Usable end-to-end for authentication, model discovery, streaming, and tools. See [Known limitations](#known-limitations).
10
10
 
@@ -24,11 +24,13 @@ instead of requiring a host-specific Cursor provider.
24
24
  | **MiMo Code** | Unchanged through OCP's OpenCode-clone compatibility layer |
25
25
  | **pi** | Unchanged through `@opencode-compat/pi-bridge` and pi's provider extension API |
26
26
  | **oh-my-pi (omp)** | Unchanged through the same Pi-family bridge |
27
+ | **DeepSeek Harness (dsh)** | Unchanged through `@opencode-compat/dsh-bridge` and DSH's Cordis `LlmAdapter` API |
27
28
 
28
29
  Start with OCP's host guides:
29
30
 
30
31
  - [OpenCode clones: Kilo Code and MiMo Code](https://github.com/oakimov/opencode-plugin-compat/blob/main/docs/hosts/opencode-clones.md)
31
32
  - [Pi family: pi and oh-my-pi](https://github.com/oakimov/opencode-plugin-compat/blob/main/docs/hosts/pi-family.md)
33
+ - [DSH family: DeepSeek Harness](https://github.com/oakimov/opencode-plugin-compat/blob/main/docs/hosts/dsh-family.md)
32
34
 
33
35
  OCP is designed to grow by adding host profiles and narrow adapters or bridges,
34
36
  so support for other coding agents can be added there while this provider and
@@ -42,7 +44,7 @@ OpenCode driving a Cursor-routed Grok model through this provider:
42
44
 
43
45
  ## Features
44
46
 
45
- - **Multi-host compatibility** — the same plugin package runs natively in OpenCode and OpenCode 2.0, and unchanged through OCP in Kilo Code, MiMo Code, pi, and oh-my-pi; new host support belongs in the compatibility layer rather than a provider fork
47
+ - **Multi-host compatibility** — the same plugin package runs natively in OpenCode and OpenCode 2.0, and unchanged through OCP in Kilo Code, MiMo Code, pi, oh-my-pi, and DeepSeek Harness; new host support belongs in the compatibility layer rather than a provider fork
46
48
  - **OpenCode integration** — registers a `cursor` provider with auth hooks and cached model list
47
49
  - **Authentication** — browser OAuth (PKCE), or API key from [cursor.com/settings](https://cursor.com/settings)
48
50
  - **Model discovery** — fetches available models from Cursor's API and caches them locally
@@ -95,7 +97,7 @@ bun install
95
97
  bun run build
96
98
  ```
97
99
 
98
- You can also install from npm (`npm install cursor-opencode-provider` or `bun add cursor-opencode-provider`). Pin a version in host config if you want, for example `cursor-opencode-provider@0.4.1`.
100
+ You can also install from npm (`npm install cursor-opencode-provider` or `bun add cursor-opencode-provider`). Pin a version in host config if you want, for example `cursor-opencode-provider@0.8.2`.
99
101
 
100
102
  ## OpenCode setup
101
103
 
@@ -115,7 +117,7 @@ Then choose a method:
115
117
  | **Cursor account (browser login)** | PKCE OAuth — opens cursor.com to sign in |
116
118
  | **API key** | Paste a key from [cursor.com/settings](https://cursor.com/settings) (`crsr_...`) |
117
119
 
118
- After login, the plugin fetches your available models and writes them to `<host-cache>/cursor-models.json`. On later startups, a missing, empty, expired, or old-schema cache is refreshed during config load when Cursor auth is available; an existing stale cache remains usable if refresh fails. Completed conversation state is stored separately under `<host-cache>/cursor-conversations/`, with one independently replaced gzip-compressed protobuf snapshot per stable host session. Snapshots are refreshed at each successful Cursor `TurnEnded` and restored after provider restarts; records not updated for more than 24 hours are removed when the provider starts. Cursor's checkpoint is an index of content-addressed KV blobs rather than a self-contained transcript, so the snapshot retains the blobs reachable from the latest checkpoint and discards superseded blobs. This private Cursor state is not a copy of the host's session database.
120
+ After login, the plugin fetches your available models and writes them to `<host-cache>/cursor-models.json`. On later startups, a missing, empty, expired, or old-schema cache is refreshed during config load when Cursor auth is available; an existing stale cache remains usable if refresh fails. Completed conversation state is stored separately under `<host-cache>/cursor-conversations/`, with one independently replaced gzip-compressed protobuf snapshot per stable host session. Snapshots are refreshed at each successful Cursor `TurnEnded`, while a turn waits on a host tool, and when OpenCode 2 stops a turn or shuts down, and restored on demand after provider restarts. A session loads only its own snapshot; it never waits for a directory-wide cleanup. Records not updated for more than 24 hours are rejected on load and collected independently using bounded reads of their persisted metadata, without relying on filesystem modification times; the same age rule collects temp files left by an interrupted save. Full payload and gzip integrity checks run when a snapshot is restored. Cursor's checkpoint is an index of content-addressed KV blobs rather than a self-contained transcript, so the snapshot retains the blobs reachable from the latest checkpoint and discards superseded blobs. This private Cursor state is not a copy of the host's session database.
119
121
 
120
122
  ### Paths (host cache)
121
123
 
@@ -146,7 +148,9 @@ opencode run --model cursor/composer-2.5 "Hello from Cursor via OpenCode"
146
148
 
147
149
  Cursor models often expose parameterized variants (effort, thinking, fast, context tier, …). The plugin materializes those as OpenCode **model variants**. In the TUI, pick one from the variant dialog or cycle with OpenCode’s `variant_cycle` keybind (default `ctrl+t`).
148
150
 
149
- The selected variant’s Cursor parameter map is forwarded on the Run as `requested_model.parameters` (isolated under `providerOptions.cursor.cursorVariantParameters` so unrelated OpenCode options are not leaked onto the wire). The provider validates that explicit selection against the current cached tuple; malformed, reordered, or stale selections fail clearly instead of silently falling back to another variant.
151
+ The selected variant’s Cursor parameter map is forwarded on the Run as `requested_model.parameters` (isolated under `providerOptions.cursor.cursorVariantParameters` so unrelated OpenCode options are not leaked onto the wire). The provider validates that explicit selection against the current cached tuple; malformed, duplicate-id, or stale selections fail clearly instead of silently falling back to another variant. Parameter order does not matter.
152
+
153
+ Without an explicit selection, an effort hint stays within the requested base or max tier and changes only the effort of that tier's default variant (thinking and other parameters keep the default's values). The provider prefers non-Fast variants within that tier and keeps Cursor's declared default effort when the default is Fast. An unavailable effort falls back to an advertised default tuple rather than inventing parameter values.
150
154
 
151
155
  #### 1M / long context
152
156
 
@@ -159,6 +163,19 @@ OpenCode’s context limit is static per model entry, while Cursor’s long-cont
159
163
 
160
164
  The Run still uses Cursor’s original model id; OpenCode’s synthetic `-1m` id is only for picking and limits.
161
165
 
166
+ Other advertised max-context tiers also get separate entries. For example,
167
+ Grok 4.7's marked 500k variants appear under `grok-4.7-500k`, with a 500,000-token
168
+ context limit; `grok-4.7-500k-fast` carries the Fast variants and pricing.
169
+ Each dedicated context entry keeps only its tier's variants and retains the declared default
170
+ effort, including when the default's non-Fast twin is needed. Non-1M entries
171
+ retain the base output limit of 32,000 tokens.
172
+
173
+ Existing non-1M variant names also remain on the original base/Fast entries so
174
+ saved selections such as `grok-4.7#Grok 4.7 High 500k` keep working after an
175
+ upgrade. These legacy selections send the selected 500k parameters and max-mode
176
+ flag, but keep the original entry's conservative static context budget. Select
177
+ `grok-4.7-500k` or `grok-4.7-500k-fast` to give OpenCode the full 500k budget.
178
+
162
179
  #### Fast variants with distinct prices
163
180
 
164
181
  When Cursor publishes a different Fast rate for a model (currently Composer and Grok), the plugin emits a separate OpenCode entry `<model-id>-fast` (for example `composer-2.5-fast`) using the same pattern as `-1m`:
@@ -179,6 +196,12 @@ The provider resolves each catalog context limit in this order:
179
196
 
180
197
  The docs fallback only fills static catalog metadata. It does not invent Cursor variants or enable a long-context tier that `AvailableModels` did not advertise.
181
198
 
199
+ #### Model families
200
+
201
+ Catalog entries derive a models.dev-style `family` from the Cursor model id (`claude-haiku-4-5` → `claude-haiku`, `gpt-5.6-luna` → `gpt-luna`). Version segments and trailing `-1m` / `-fast` suffixes do not split families; Kimi keeps its major generation (`kimi-k2.7-code` → `kimi-k2`). Auto (`default`) has no derived family. A non-empty family supplied in cached model metadata takes precedence; the live `AvailableModels` schema has no family field.
202
+
203
+ OpenCode uses family metadata to select a small model for session titles (2.0: `gpt-luna`, `gemini-flash-lite`, `gemini-flash`, `claude-haiku`; 1.x: `gemini-flash`, `gpt-nano`, `claude-haiku`). If no eligible family is available, titles fall back to the session model. To override this selection, set the `title` agent's `model` on either major, or `small_model` on 1.x. Compaction summaries use the host's compaction model selection; this metadata does not move them to the small model. OpenCode 2's model-listing tool shows one entry per family unless asked for all.
204
+
182
205
  #### Image input
183
206
 
184
207
  The provider advertises `text` + `image` input only when the selected Cursor model supports images; model output remains text. This works in the classic plugin, the 1.18 v2 plugin, and OpenCode 2.0.
@@ -189,14 +212,19 @@ Image support is resolved in this order:
189
212
  2. The `Capabilities` column in Cursor's [`docs.md`](https://cursor.com/docs.md), generated alongside context and pricing metadata
190
213
  3. Text-only when neither source describes the model
191
214
 
192
- OpenCode image file parts are decoded from bytes, base64/data URLs, local file URLs, or HTTP(S) URLs and sent through Cursor's `UserMessage.selected_context.selected_images` field. On fresh or rebased Runs, the provider also harvests image `file-data` from tool results and `file` parts from assistant and historical user messages (the trailing user message stays owned by last-user extraction); previously sent history images are deduplicated by content hash per OpenCode session. Held-open continuation results remain text-only because Cursor's exec-result channel has no image field. Attachments are limited to 20 MiB total, matching OpenCode's desktop attachment budget. PDF, audio, and video inputs are not advertised or silently discarded.
215
+ OpenCode image file parts are decoded from bytes, base64/data URLs, local file URLs, or HTTP(S) URLs and sent through Cursor's `UserMessage.selected_context.selected_images` field. Fresh and rebased Runs normalize the same image formats as held-Run continuations, including AI SDK `image-data` / `image-url` and OpenCode attachment URLs. A Run that resumes a checkpoint skips the history images that exact checkpoint is known to hold (by content hash, recorded when a turn ends on a checkpoint of its own and kept in the restart snapshot); a Run without a checkpoint sends them all.
216
+
217
+ On a held-open Run, an image read returns bytes in `ReadSuccess.data` (including GenerateImage reference reads), and MCP tool images return as `McpImageContent` items after the success text (`McpToolResultContentItem` field 2). Detached media is attributed only where OpenCode's result order and markers establish its owner; ambiguous images stay available through history instead of being attached to an unrelated tool. A host instruction update beside a native read uses Cursor's context-injection channel because read text and bytes are mutually exclusive.
218
+
219
+ One 20 MiB decoded-byte budget covers all image-bearing pending results in a continuation, without Cursor CLI's resize/re-encode step. Unreadable or oversized tool images are reported without blocking other results; cancellation still stops decoding. Other result shapes have no image field, so their images reach Cursor as history on the next fresh Run. Direct PDF, audio, and video attachments are unsupported; tool-result delivery only forwards image media.
193
220
 
194
221
  #### Max mode
195
222
 
196
- Cursor IDE has a separate **Max Mode** toggle that sets `requested_model.max_mode` and selects the default max / 1m variant. OpenCode has no equivalent custom toggle, so this provider approximates it:
223
+ Cursor IDE has a separate **Max Mode** toggle that sets `requested_model.max_mode` and selects the default max variant. OpenCode has no equivalent custom toggle, so this provider approximates it:
197
224
 
198
- - Selecting a `*-1m` model (or any resolved params with `context=1m`) sets wire `max_mode` to `true`
199
- - An explicit `providerOptions.cursor.maxMode` hint also turns it on
225
+ - Selecting a variant that Cursor's model list marks as max mode sets wire `max_mode` to `true`, as Cursor CLI does. That covers `*-1m` models and other max tiers such as Grok 4.7's 500k variants; a variant without the marker falls back to `context=1m`
226
+ - A model Cursor reports as not supporting non-max mode always runs in max mode
227
+ - An explicit `providerOptions.cursor.maxMode` hint without a picked variant also turns it on and selects the declared max default, using its non-Fast twin when available. A tier with only Fast variants remains selectable
200
228
 
201
229
  There is no independent Max Mode chrome in OpenCode beyond choosing the 1M model / long-context variant.
202
230
 
@@ -211,16 +239,135 @@ const cursor = createCursor({
211
239
  // apiBaseURL: "https://api2.cursor.sh",
212
240
  // agentBaseURL: "https://agentn.us.api5.cursor.sh", // explicit Run host override
213
241
  // telemetryEnabled: true, // opt in to GetServerConfig telemetry
214
- // cacheDir: "/path/to/host/cache", // optional; else active-host heuristic
215
- // retry: { maxAttempts: 3, baseDelayMs: 500, maxDelayMs: 8_000 },
216
- // continuation: { heartbeatMs: 5_000, semanticIdleMs: 120_000, hardCapMs: 600_000 },
242
+ // cacheDir: "/path/to/host/cache", // optional; else path bridge or native OpenCode cache
243
+ // retry: { maxAttempts: 3, baseDelayMs: 500, maxDelayMs: 8_000 }, // optional stricter total limit
244
+ // continuation: { heartbeatMs: 5_000, semanticIdleMs: 30_000, hardCapMs: 600_000 },
217
245
  })
218
246
 
219
247
  const model = cursor.languageModel("composer-2.5")
220
248
  // model implements AI SDK LanguageModelV3 (doStream / doGenerate)
221
249
  ```
222
250
 
223
- Give one credential source. The provider uses the first present and never falls back from one to another: `getAccessToken` (an async function returning the current access token, called for every Run so a long-running process can renew its login; it receives `{ forceRefresh: true }` once after Cursor rejects a token), then `accessToken` (a JWT sent as-is, never renewed), then `apiKey` (a raw `crsr_...` key, exchanged for a JWT and exchanged again near its expiry, the way Cursor CLI renews an API-key login). Optional: `apiBaseURL`, `agentBaseURL`, `cacheDir`, `headers`, `telemetryEnabled`, `retry`, and `continuation`. `cacheDir` pins the host cache root for model/version caches and Cursor project metadata; when omitted, the provider uses an injected structural host path bridge or the native OpenCode XDG cache described in [Paths](#paths-host-cache). Transient failures resume from the latest checkpoint produced by that Run, matching Cursor CLI; without an eligible checkpoint, retries remain limited to replay-safe attempts so completed text or tool work is not duplicated. Once that budget is spent, or replay is unsafe, the terminal error does not re-arm OpenCode's outer retry loop. Pending-tool inactivity is renewed by OpenCode activity from the session or its descendants. The older `baseURL` option is still accepted as a legacy alias for `agentBaseURL`.
251
+ Give one credential source. The provider uses the first present and never falls back from one to another: `getAccessToken` (an async function returning the current access token, called for every Run so a long-running process can renew its login; it receives `{ forceRefresh: true }` once after Cursor rejects a token), then `accessToken` (a JWT sent as-is, never renewed), then `apiKey` (a raw `crsr_...` key, exchanged for a JWT and exchanged again near its expiry, the way Cursor CLI renews an API-key login). Optional: `apiBaseURL`, `agentBaseURL`, `cacheDir`, `headers`, `telemetryEnabled`, `retry`, and `continuation`. `cacheDir` pins the host cache root for model/version caches and Cursor project metadata; when omitted, the provider uses an injected structural host path bridge or the native OpenCode XDG cache described in [Paths](#paths-host-cache). An untouched checkpoint resume may renew a rejected credential once, while keeping completed tools behind the replay barrier. Follow-up Runs retain checkpoints received while opening. Transient failures resume from the latest checkpoint produced by that Run, matching Cursor CLI; without an eligible checkpoint, retries remain limited to replay-safe attempts so completed text or tool work is not duplicated. See [retry limits](#retry-limits) for the shared provider/host budget and endless retry option. Once that budget is spent, or replay is unsafe, the terminal error does not re-arm OpenCode's outer retry loop. Pending-tool inactivity is renewed by OpenCode activity from the session or its descendants. The older `baseURL` option is still accepted as a legacy alias for `agentBaseURL`.
252
+
253
+ ### Shell results
254
+
255
+ A failed host shell call reaches Cursor once as stderr in streamed and unary
256
+ results. Distinct partial command output remains on stdout. See the
257
+ [1.x shell guide](docs/opencode-1.md#shell-failures-and-interruption) and
258
+ [2.0 tool guide](docs/opencode-2.md#feature-parity-vs-the-classic-plugin).
259
+
260
+ ### Runtime folders
261
+
262
+ `agent-tools/` and `terminals/` live under the provider's per-workspace project
263
+ root in the host cache. The provider creates these private directories itself
264
+ and handles recognized catalog-spill writes and metadata reads directly on
265
+ Cursor's protocol. Directory creation, spills, and terminal polling do not
266
+ invoke OpenCode's workspace write/read tools or raise approval prompts. Only
267
+ the exact files `agent-tools/<uuid>.txt` and `terminals/<shell-id>.txt` are
268
+ handled this way; the folders themselves, other names in them, and paths that
269
+ resolve elsewhere go to OpenCode's own tools.
270
+ Workspace files, shell commands, generated image content, and plan execution
271
+ still use their existing host approval paths.
272
+
273
+ Explicit and soft-background shells keep a durable
274
+ `terminals/<shell-id>.txt` record containing status, output, command, cwd, PID,
275
+ timing, and the real exit code. Shell IDs are separate from PIDs. Output
276
+ reaches the record as the command writes it; the header is updated in place
277
+ every second, so a long-running command is never copied again. A soft-background
278
+ handoff returns the output of its foreground window, and a background notice
279
+ names the shell ID and its record. A detached supervisor owns the command
280
+ group, drains descendant output for up to five seconds after command exit, and
281
+ finalizes cancellations and hard timeouts, allowing the group three seconds
282
+ between TERM and KILL. If the supervisor itself dies (killed, crashed, or lost
283
+ to a reboot), the group is stopped and the record is reported as aborted with
284
+ an unknown exit status. Successful completion preserves deliberately detached
285
+ daemons, matching native OpenCode; cancellation, hard timeouts, and failed
286
+ commands stop their command group. The payload runs under the shell selected
287
+ by OpenCode, so bash and zsh syntax retains its native meaning.
288
+ A collector using the provider's JavaScript runtime
289
+ stores at most 8 MiB of command output and drains excess bytes without writing
290
+ them; the terminal header and completion footer are additional metadata.
291
+ Catalog spills are bounded to 8 MiB per file and 64 MiB / 128 files per project,
292
+ with at most one additional 8 MiB temporary file during atomic replacement;
293
+ reads to 1 MiB with an explicit partial-content notice. Records and job data
294
+ untouched for seven days are removed; a live supervisor refreshes its record
295
+ every second. Completed jobs discard auxiliary launch files immediately and
296
+ remaining job metadata after one minute; their terminal records stay readable.
297
+ Permission rejection and interrupted tools release unstarted reservations
298
+ without waiting for another model turn.
299
+ Cleanup runs during runtime operations as well as workspace discovery, at
300
+ most once a minute per project.
301
+ A terminal storage failure keeps captured output readable with an incompleteness
302
+ notice; a size-cap notice is added only when the returned content was capped. A spill storage failure returns an error instead of requesting
303
+ approval for a fallback write; folders removed from the cache are recreated.
304
+
305
+ A spawned background command is launched by a host command that contains the
306
+ command verbatim, so the host approval shows exactly what runs. A
307
+ soft-background command keeps its own text: the plugin's shell hooks run it
308
+ under the supervisor. On OpenCode 2, the shell create hook installs an
309
+ executable adapter while preserving the original command for permission
310
+ scanning; on classic bash/zsh the post-approval shell environment hook installs
311
+ the launcher. Other POSIX shells receive the complete inline launcher for
312
+ approval. A host without those hooks runs soft-background calls in the
313
+ foreground. The launcher's startup and foreground-window deadlines each run
314
+ on a single timer, so it reports within the host's own timeout; a spawn
315
+ reports its shell ID even when the command has already finished. A background
316
+ request made through OpenCode 2's own shell `background` option stays an
317
+ OpenCode background job with the host's completion notice. Supervision
318
+ requires POSIX bash or `setsid`. When this conversation has no usable terminal storage, the unmanaged `nohup`
319
+ fallback requires a POSIX shell. Native Windows PowerShell/`cmd` background
320
+ wrapping is unsupported; see [Known limitations](#known-limitations).
321
+ Transcript storage is supplied
322
+ separately through `CURSOR_AGENT_TRANSCRIPTS_FOLDER`; the provider neither
323
+ creates that directory nor writes host transcripts into it.
324
+
325
+ ## Retry limits
326
+
327
+ Following Cursor CLI, each user turn allows **10 transport retries** and
328
+ **3 server retries**, counted separately. Connection failures and a stream ending
329
+ without `turn_ended` use the transport budget; server failures and the inbound
330
+ read-idle deadline use the server budget. Retries of the same turn share these
331
+ counts across checkpoints, tool steps and OpenCode's own retry calls. A new user
332
+ turn, successful completion or Stop starts a fresh budget. Counts are held in memory for the running process.
333
+ OpenCode 2's interruption notices keep the same budget as the original request,
334
+ including when a plugin appends recognized `<system-reminder>` or
335
+ `<system-update>` text parts. These notes remain in the request sent to Cursor;
336
+ attachments and ordinary follow-up text still identify a new request.
337
+ OpenCode may stop earlier under its own retry policy.
338
+
339
+ Like Cursor CLI, an active Run with no pending host work fails after **30 seconds
340
+ without an inbound frame**. Every inbound frame, including a heartbeat or partial
341
+ tool input, renews this deadline; client heartbeats do not. Silence uses the
342
+ transport retry budget and the 180-second consecutive-stall window. The separate
343
+ 120-second transport read-idle watchdog remains a backstop. Direct SDK callers
344
+ can override `continuation.semanticIdleMs` (or its `softHealthMs` alias).
345
+
346
+ If a Run drops immediately after completed tool results, before new output,
347
+ the provider can rebuild from host history containing every observed exec's
348
+ result. It sends the completed results once and asks Cursor to continue. A
349
+ checkpoint predating those results is never resumed. Missing results, unknown
350
+ activity or a new interaction keep the failure final.
351
+ A checkpoint received while the completed result drains remains eligible.
352
+ History recovery retains completed results from earlier resumed Runs too.
353
+
354
+ For direct SDK use, `retry.maxAttempts` adds a total attempt ceiling, including
355
+ the initial attempt; omitting it uses the two Cursor CLI budgets. The existing
356
+ backoff defaults remain 500 ms full jitter, growing exponentially to an 8 s
357
+ ceiling (`baseDelayMs` / `maxDelayMs`). OpenCode controls delays when it owns
358
+ the retry after partial output. An unfinished tool display row, without an exec
359
+ or started tool lifecycle, also permits a host retry: the row closes as unfinished
360
+ and has no tool side effect to repeat.
361
+
362
+ Set `CURSOR_ENDLESS_RETRIES=1` or `true` before starting OpenCode (or its daemon)
363
+ to bypass both count limits and `retry.maxAttempts`, like Cursor CLI's endless
364
+ retry mode. This keeps retrying eligible transient failures; it preserves Stop,
365
+ final errors, authentication rules and tool replay guards. An upload with
366
+ uncertain delivery ends the turn even when an older checkpoint exists.
367
+ Cursor CLI's safety cutoffs remain: two resumed attempts that stream without a
368
+ new checkpoint end the turn, and consecutive semantic-stall retries stop once
369
+ 180 seconds have elapsed since the first stall retry. A non-stall failure resets
370
+ that stall window. The setting does not override OpenCode's own retry limits.
224
371
 
225
372
  ## Environment variables
226
373
 
@@ -231,29 +378,35 @@ Give one credential source. The provider uses the first present and never falls
231
378
  | `CURSOR_GET_SERVER_CONFIG_TELEMETRY` | Set to `1` or `true` to opt the `GetServerConfig` lookup into telemetry in OpenCode/plugin usage |
232
379
  | `HTTPS_PROXY` / `https_proxy` | HTTP CONNECT proxy for HTTPS. Bun `fetch` uses this for unary RPCs; the Run HTTP/2 session also tunnels through it (see `NO_PROXY`). The Run tunnel supports only `http://` proxy URLs (Basic auth via userinfo); `https://` and SOCKS proxy URLs leave the Run on a direct connection. `HTTP_PROXY` is not used for HTTPS |
233
380
  | `NO_PROXY` / `no_proxy` | Comma/space hosts that bypass the proxy (`*`, exact host, or `.suffix` / `suffix` domain forms; a `host:port` entry bypasses only that port) |
381
+ | `CURSOR_ENDLESS_RETRIES` | Set to `1` or `true` to bypass retry count limits; off by default. Safety cutoffs and host retry limits still apply. See [retry limits](#retry-limits). |
234
382
  | `CURSOR_PROVIDER_DEBUG` | Set to `1` or `true` to enable wire-level debug logging |
235
383
  | `CURSOR_PROVIDER_DEBUG_FILE` | Debug log path (default: `debug-<pid>.log` under `$TMPDIR/cursor-provider-logs-<uid>/`) |
384
+ | `CURSOR_AGENT_TRANSCRIPTS_FOLDER` | Optional folder containing past-chat transcripts, advertised to Cursor on OpenCode 1.x and 2.0. Sent only if it is an existing directory; unset, empty, missing, non-directory, or unstatable paths are omitted. Relative paths resolve against the workspace. The provider does not create the folder or write transcripts. |
236
385
  | `CURSOR_OPENCODE2_DEV_ENTRY` | **Local OpenCode 2.0 only.** Absolute path to a built entry file (usually `dist/index.js`). Rewrites the AI SDK package to `aisdk:file://…` so the daemon imports your local build instead of `npm install`-ing the published package. Export it **before** `opencode2 service start`, then restart after rebuilds. Unset in production. See [OpenCode 2.0 local clone](docs/opencode-2.md#from-a-local-clone-cursor_opencode2_dev_entry). |
237
386
  | `CURSOR_OPENCODE2_TODOS` | **OpenCode 2.0 only.** Set to `1` or `true` to register plugin-owned `todowrite`/`todoread` (in-memory; no TUI sidebar). Off by default. OpenCode 1.x still uses the host builtin. |
238
387
  | `XDG_CACHE_HOME` | Base for native OpenCode cache (`$XDG_CACHE_HOME/opencode/`) when no explicit `cacheDir` or structural host path bridge is installed |
239
388
  | `XDG_DATA_HOME` | When set, OpenCode `auth.json` is read from `$XDG_DATA_HOME/opencode/` instead of `~/.local/share/opencode/` |
240
389
 
390
+ Set `CURSOR_AGENT_TRANSCRIPTS_FOLDER` before starting OpenCode (or its daemon). The folder is checked when a conversation's RequestContext base is first built and stays frozen for that conversation; start a new session after changing the setting.
391
+
241
392
  `createCursor({ agentBaseURL })` overrides the agent Run host. When unset, the provider resolves the host from Cursor's `GetServerConfig` API (`agentUrlConfig.agentnUrl`, region-specific — e.g. `agentn.us.api5.cursor.sh`, `agent-gcpp-uswest.api5.cursor.sh`) once per process and holds it in memory (never written to disk), so a held-open Run stream is never repointed mid-session. Explicit agent overrides and GetServerConfig results are validated as HTTPS `*.cursor.sh` hosts (Cursor's agent hostnames vary and may change); non-`cursor.sh` hosts are rejected. Shared HTTP/2 connections are rotated before they become server-aged, while existing Runs may finish on their original connection. The lookup sends `{ "telem_enabled": false }` by default; set `telemetryEnabled: true` in provider config, or `CURSOR_GET_SERVER_CONFIG_TELEMETRY=1` for OpenCode/plugin usage, to opt in. If the lookup fails or does not return a valid Cursor agent host, the model call fails clearly instead of falling back to `agentn.global.api5.cursor.sh`.
242
393
 
243
394
  ## Development
244
395
 
396
+ Changes listed under [Unreleased](CHANGELOG.md#unreleased) describe the development checkout; use a rebuilt local install to test them before a release. Maintainer guides and incident lessons are linked from the [documentation index](docs/README.md).
397
+
245
398
  ```bash
246
399
  bun install # install dependencies
247
400
  bun run build # compile TypeScript → dist/
248
401
  bun run typecheck # type-check without emit
249
402
  bun test # run unit tests
250
- bun run test:node-http2 # Node-specific HTTP/2 detach regression
403
+ bun run test:node-http2 # Node HTTP/2 cleanup, drain, cancellation and memory regressions
251
404
  bun run test:watch # watch mode
252
405
  bun run generate:pricing # refresh src/pricing-data.ts from Cursor docs
253
406
  bun run check:pricing # fixture coverage for known model ids
254
407
  ```
255
408
 
256
- **HARD STOP before any version bump or `v*` tag:** run `generate:pricing` and `check:pricing` in the same session, fix any unmapped Cursor display names, and commit mapping/`pricing-data.ts` updates **before** touching `"version"` or creating the tag. Publish CI regenerates rates from the live docs and will fail the release on an unmapped name — that is a last line of defense, not the primary check. See `AGENTS.md`.
409
+ **HARD STOP before any version bump or `v*` tag:** run `generate:pricing` and `check:pricing` in the same session, fix any unmapped Cursor display names, commit mapping/`pricing-data.ts` updates, and finalize [`CHANGELOG.md`](CHANGELOG.md) (`[Unreleased]` → dated version) **before** touching `"version"` or creating the tag. Publish CI regenerates rates from the live docs and will fail the release on an unmapped name — that is a last line of defense, not the primary check. See `AGENTS.md` and [Changelog](#changelog).
257
410
 
258
411
  ## Architecture
259
412
 
@@ -270,7 +423,7 @@ OpenCode
270
423
  |--------|------|
271
424
  | `src/plugin.ts` | Classic OpenCode hooks: provider registration, OAuth, API key exchange, login renewal and persistence |
272
425
  | `src/plugin-v2.ts` | OpenCode 1.18 Effect/Promise v2 plugin (`ctx.aisdk.*`); load via `./plugin/v2` only |
273
- | `src/plugin-opencode2.ts` | OpenCode 2.0 plugin (`provider.transform`, integration, tools, aisdk, `shell.create.before`, `websearch`, plan kickoff); load via `./plugin/opencode2` or `./server` |
426
+ | `src/plugin-opencode2.ts` | OpenCode 2.0 plugin (`provider.transform`, integration, tools, aisdk, `shell.create.before`, `websearch`); load via `./plugin/opencode2` or `./server` |
274
427
  | `src/opencode2/` | 2.0-only provider inventory, integration/auth, and local API types |
275
428
  | `src/plugin-core.ts` | Host-neutral SDK factory, package matching, API base/telemetry resolution |
276
429
  | `src/model-config.ts` | Cursor model → OpenCode model mapping shared by every plugin surface |
@@ -283,7 +436,8 @@ OpenCode
283
436
  | `src/auth.ts` | PKCE OAuth, API key exchange, browser-login session refresh (`/oauth/token`) |
284
437
  | `src/auth-renewal.ts` | Credential renewal policy: session and API-key renewal kept separate, backoff, on-demand renewal, Bearer resolution |
285
438
  | `src/models.ts` | `AvailableModels` fetch and `cursor-models.json` cache |
286
- | `src/protocol/conversation-persistence.ts` | Atomic per-session restart snapshots under the 24-hour `cursor-conversations/` cache |
439
+ | `src/protocol/conversation-persistence.ts` | Atomic per-session restart snapshots, on-demand hydration and independent 24-hour cache maintenance |
440
+ | `src/protocol/conversation-cache-header.ts` | Bounded streaming inspection of persisted snapshot metadata without decoding checkpoint/blob payloads |
287
441
  | `src/context/paths.ts` | Host cache root + Cursor project metadata under `<host-cache>/projects/<slug>/` |
288
442
  | `src/agent-url.ts` | `GetServerConfig` fetch + in-process memo (region-specific Run host) |
289
443
  | `src/transport/connect.ts` | HTTP/2 bidi stream and unary RPC calls |
@@ -292,11 +446,13 @@ OpenCode
292
446
 
293
447
  ### Injected system guidance
294
448
 
295
- The provider adds OpenCode-specific system guidance to normal tool-capable conversations, including tool availability, canonical workspace-path grounding, preferring OpenCode `todowrite` / `todoread` over Cursor TodoWrite when those tools are advertised without inviting catalog-comparison narration, and preferring `edit` / `write` (or `apply_patch`, when the host advertises that instead) over shell-based file mutation when those tools are available. Compaction keeps its dedicated prompt unchanged.
449
+ The provider adds OpenCode-specific system guidance to normal tool-capable conversations, including tool availability, canonical workspace-path grounding, preferring OpenCode `todowrite` / `todoread` over Cursor TodoWrite when those tools are advertised without inviting catalog-comparison narration, and preferring `edit` / `write` (or the advertised patch tool: `apply_patch` on OpenCode 1.x, `patch` on OpenCode 2.0) over shell-based file mutation when those tools are available. Compaction keeps its dedicated prompt unchanged.
296
450
 
297
- Cursor keeps OpenCode `skill` and configured MCP tools off its native top-level function list on **both** OpenCode 1.x and OpenCode 2.0 — they appear as names-only MCP descriptors and are reached through GetDynamicTools / CallDynamicTool (exec #36 supplies full definitions). When the host advertises `skill` and/or tools of MCP servers configured in `opencode.json`, the provider names those routes in system guidance and mirrors OpenCode's load-when-matches / do-not-reinvoke rules so models prefer `skill` / MCP over Grep/Shell and do not narrate that those tools are unavailable. Skill catalog changes after admission are host `<system-update>` notes, not a second RequestContext skill list. OpenCode 2 still moves MCP servers that did not set `"codemode": true` onto the direct AI SDK catalog first; without that placement they never reach Cursor's MCP ads at all (see [OpenCode 2 MCP tools](docs/opencode-2.md#mcp-tools)).
451
+ Cursor keeps OpenCode `skill` and configured MCP tools off its native top-level function list on **both** OpenCode 1.x and OpenCode 2.0 — they appear as names-only MCP descriptors and are reached through GetDynamicTools / CallDynamicTool (exec #36 supplies full definitions). When the host advertises `skill` and/or tools of MCP servers configured in `opencode.json`, the provider names those routes in system guidance and mirrors OpenCode's load-when-matches / do-not-reinvoke rules so models prefer `skill` / MCP over Grep/Shell and do not narrate that those tools are unavailable. When the host advertises `skill`, skills that have a usable file path are also listed in RequestContext `agent_skills` (path and description only), as Cursor's own clients list them. Skill catalog changes after admission are host `<system-update>` notes; `agent_skills` follows the baseline catalog until the next epoch. OpenCode 2 still moves MCP servers that did not set `"codemode": true` onto the direct AI SDK catalog first; without that placement they never reach Cursor's MCP ads at all (see [OpenCode 2 MCP tools](docs/opencode-2.md#mcp-tools)).
298
452
 
299
- Native Cursor todo display updates (`update_todos_tool_call` / `read_todos_tool_call`) are mirrored into `todowrite` / `todoread` when those host tools are advertised. OpenCode 2 dropped the host builtins; set `CURSOR_OPENCODE2_TODOS=1` (or `true`) to register the same canonical names when the catalog omits them (in-memory per session, cleared on `session.deleted` and process restart). With the flag unset, the plugin registers no todo tools. Enabled plugin tools are **direct** catalog tools (`options.codemode: false` plus an `output` schema), not Code Mode helpers. The provider retains the last known full list per OpenCode session — refreshed by bridged writes, direct host `todowrite` calls, and `todoread` results, surviving Run close, checkpoint resume, and later turns — so `merge: true` updates that omit Cursor's completed list still become a host replace-all snapshot (matched by id, then by content across id spaces). This snapshot is in-memory only and never enters the prompt, RequestContext, or persisted restart state, so Cursor's prompt-cache prefix is unaffected; a provider process restart clears it until the next write/read (intentional — see `tasks/lessons.md`).
453
+ Each discovered dynamic definition carries its exact `namespace` and `toolName` in its description. Keep those fields at the outer level of every `CallDynamicTool` call; the discovered input schema describes only its `arguments` object.
454
+
455
+ Native Cursor todo display updates (`update_todos_tool_call` / `read_todos_tool_call`) are mirrored into `todowrite` / `todoread` when those host tools are advertised. OpenCode 2 dropped the host builtins; set `CURSOR_OPENCODE2_TODOS=1` (or `true`) to register the same canonical names when the catalog omits them (in-memory per session, cleared on `session.deleted` and process restart). With the flag unset, the plugin registers no todo tools. Enabled plugin tools are **direct** catalog tools (`options.codemode: false` plus an `output` schema), not Code Mode helpers. The provider retains the last known full list per OpenCode session — refreshed by bridged writes, direct host `todowrite` calls, and `todoread` results, surviving Run close, checkpoint resume, and later turns — so `merge: true` updates that omit Cursor's completed list still become a host replace-all snapshot (matched by id, then by content across id spaces). This snapshot is in-memory only and never enters the prompt, RequestContext, or persisted restart state, so Cursor's prompt-cache prefix is unaffected; a provider process restart clears it until the next write/read (intentional — see [todo lessons](tasks/lessons/opencode.md#boundaries-and-tool-contracts)).
300
456
 
301
457
  If this guidance causes issues, update `buildOpenCodeInteractionGuidance` in [`src/language-model.ts`](src/language-model.ts) and its focused coverage in [`test/prompt-history.test.ts`](test/prompt-history.test.ts).
302
458
 
@@ -304,22 +460,39 @@ If this guidance causes issues, update `buildOpenCodeInteractionGuidance` in [`s
304
460
 
305
461
  Cursor's legacy `edit_tool_call` is implemented internally as a correlated `read_args` followed by a whole-file `write_args`; the write is transport mechanics, not a new decision to replace OpenCode's `edit` tool. For regular files inside the workspace, the provider answers that private prerequisite read directly with the complete file (up to Cursor's own 50 MB edit limit), because routing it through OpenCode's ordinary 50 KB-capped `read` tool makes Cursor calculate a truncated replacement. This shortcut is restricted to the exact path named by the active edit call and rejects symlink escapes. External paths first take OpenCode's permission-aware read path; after that exact read succeeds, the provider upgrades its private result to complete content so an explicitly authorized external edit does not inherit OpenCode's preview cap.
306
462
 
307
- The provider retains the correlation and exposes the resulting existing-file mutation as one unique, line-bounded OpenCode `edit`. It still returns Cursor's expected typed `write_result`. New-file creation remains a `write`, and an `apply_patch`-only catalog receives the same targeted change as an `*** Update File:` patch.
463
+ The provider retains the correlation and exposes the resulting existing-file mutation as one unique, line-bounded OpenCode `edit`, including turns that permit `edit` but withhold `write`. It still returns Cursor's expected typed `write_result`. When only a patch tool is permitted, that targeted edit becomes an `*** Update File:` patch. New-file creation uses `write` when permitted, otherwise the permitted patch tool's `*** Add File:` operation.
308
464
 
309
465
  Ordinary complete native reads also preserve an existing final LF/CRLF. OpenCode's numbered read envelope cannot represent that terminator, and dropping it made exact edit matching fail. This restoration applies only to confirmed-complete, unbounded reads; paged and capped reads keep their existing truncation behavior.
310
466
 
311
- ### `apply_patch` models (GPT-5 and friends)
467
+ ### `apply_patch` / `patch` on GPT models
468
+
469
+ OpenCode selects the file-editing tools before calling this provider:
470
+
471
+ | Host | Patch tool | Input |
472
+ |------|------------|-------|
473
+ | OpenCode 1.x | `apply_patch` | `{ patchText }` |
474
+ | OpenCode 2.0 | `patch` | `{ patchText }` |
475
+
476
+ Both hosts replace `edit` / `write` with their patch tool when the API model id contains `gpt-`, excluding ids containing `oss` or `gpt-4`. Other models normally keep `edit` / `write`. This is host behavior; the provider selects its translation from the current turn's permitted tools, including after a model switch, without branching on a model id or host version.
477
+
478
+ The host's patch tool is advertised to Cursor through the dynamic tool catalog and can be called directly. Cursor also sends native write/edit exec requests, which the provider converts before checking tool availability when the corresponding `write` or `edit` tool is absent:
312
479
 
313
- OpenCode 1.x does not simply add `apply_patch` for GPT-series models — it **removes** `edit` and `write` from the tool catalog and advertises `apply_patch` in their place (`ToolRegistry.tools`; the model id must contain `gpt-` and neither `oss` nor `gpt-4`). Cursor keeps sending its native write/edit requests regardless, so without translation every file change on a `gpt-5*` model is refused as an unavailable tool.
480
+ - An uncorrelated text write becomes `*** Add File:`, which can create a file or intentionally overwrite its complete contents.
481
+ - A correlated legacy edit or native substring edit becomes a targeted `*** Update File:` patch.
482
+ - If the requested native tool is permitted, it remains `write` or `edit`; if neither it nor a patch tool is permitted, the request is refused.
314
483
 
315
- The provider translates transparently: an uncorrelated native write becomes an `*** Add File:` patch, while a correlated legacy edit or Pi edit becomes a minimal `*** Update File:` chunk widened to whole lines. This is keyed purely off what the host advertises — it is inert whenever `edit`/`write` are offered normally, and inert again when the host offers neither those nor `apply_patch`. An edit that cannot be expressed faithfully (target unreadable, or the text to replace is absent or ambiguous) is refused with a specific message rather than applied to the wrong region.
484
+ Blank lines and representable line endings are preserved. Conversions that cannot preserve the requested operation return a specific error before a host mutation is emitted. These include paths containing newlines or trimmed whitespace, unreadable or ambiguous edits, and nonempty resulting content without a final newline.
316
485
 
317
- OpenCode 2.0 does not perform this substitution, so the path is 1.x-only in practice. See [`src/protocol/apply-patch.ts`](src/protocol/apply-patch.ts) for the upstream references.
486
+ On 1.x, translated edits normalize their old/new text to the existing file's LF/CRLF convention, matching native `edit` even when Cursor sends an LF-only correlated write. Direct `apply_patch` updates also restore CRLF in context and added lines. Direct updates of mixed-ending or unterminated files are refused when their bytes cannot be safely preserved; use a Cursor-native targeted edit for mixed-ending files. Intentional whole-file writes and `*** Add File:` payloads keep the requested line endings.
487
+
488
+ OpenCode 2's `patch` parser strips CRLF carriage returns, so both native conversions and direct `patch` calls that would lose them are refused; the provider leaves the file unchanged. This restriction belongs to the patch path, not to native `edit` / `write` when those tools are permitted. See [`src/protocol/apply-patch.ts`](src/protocol/apply-patch.ts) for host-source references and [`remapEditToolsForCatalog`](src/protocol/tools.ts) for the translation.
318
489
 
319
490
  ### Native subagent routing
320
491
 
321
492
  The provider reads the permission-filtered subagent catalog from the current host `task` or `subagent` definition and advertises those recipients to Cursor as custom subagents. Native Cursor Task exec remaps onto `task` when advertised, otherwise onto OpenCode 2 `subagent` (`agent` / `sessionID` / `background`). Exact configured names are preserved; unknown future Cursor subtype strings fall back to `general`. If a complete catalog omits every compatible recipient, the request fails on Cursor's typed subagent channel instead of selecting an arbitrary specialist.
322
493
 
494
+ When Cursor's Task attaches files, Cursor first reads each one and would hand the bytes only to its own subagent context, which OpenCode's subagent tools cannot take. For reads matching that Task's attachment list, the provider checks the target and returns placeholder image bytes without reading its content, including for runtime metadata attachments. Missing, invalid or unstatable targets receive typed failures. The child prompt lists absolute paths as JSON strings, preserving filename spaces and escaping line breaks; the child opens them with its own permission-checked tools. The parent gets no extra attachment-read step, and the host call retains Cursor's description. Image inspection requires an image-capable model in the child; a placeholder does not enable vision on a text-only model.
495
+
323
496
  | Host | Executor | Recipients |
324
497
  |------|----------|------------|
325
498
  | OpenCode 1.x | `task` | Permission-filtered builtins such as `general` / `explore`, plus configured subagents |
@@ -337,6 +510,9 @@ Primary modes and hidden/internal agents are never selected unless OpenCode expl
337
510
  | `cursor-opencode-provider/plugin/opencode2` | OpenCode 2.0 plugin (self-registering: models + auth + tools) |
338
511
  | `cursor-opencode-provider/server` | Same OpenCode 2.0 entry; OpenCode 2 Host.resolve and OpenCode 1.18 `exports["./server"]` |
339
512
  | `cursor-opencode-provider/errors` | Structured provider error classes |
513
+ | `cursor-opencode-provider/models` | Model discovery and cache APIs |
514
+ | `cursor-opencode-provider/pricing` | Pricing and capability lookup APIs |
515
+ | `cursor-opencode-provider/auth` | Authentication APIs |
340
516
  | `cursor-opencode-provider/image-save` | Host-neutral `executeCursorImageSave` (pi-bridge / non-plugin hosts) |
341
517
 
342
518
  The package root intentionally stays plugin-safe for OpenCode's classic loader. `CursorPluginV2` and non-plugin runtime APIs are **not** re-exported from the package root; load them through their dedicated subpaths.
@@ -351,14 +527,14 @@ The package root intentionally stays plugin-safe for OpenCode's classic loader.
351
527
  | Local OpenCode 2.0 still runs the published package | Set `CURSOR_OPENCODE2_DEV_ENTRY` to an absolute `…/dist/index.js` path **before** starting the daemon, persist it with `opencode2 service set env CURSOR_OPENCODE2_DEV_ENTRY …`, rebuild (`bun run build`), then `opencode2 service restart`. Loading only `dist/plugin-opencode2.js` is not enough — without the env var, 2.0 still `npm install`s the published package into the host cache. |
352
528
  | “Too many connections from different devices” | Device IDs are derived from stable OS identifiers (same approach as the Cursor CLI). Avoid running multiple clients that invent different machine fingerprints for the same account. |
353
529
  | Empty or stale model list | Delete `<host-cache>/cursor-models.json` (native default `~/.cache/opencode/`) and restart OpenCode. A compatibility layer may supply a different host-cache root through the structural path bridge. Existing Cursor auth is enough to refill the cache; re-login only if auth itself is broken. Cache TTL is 24h; a failed background refresh keeps serving the previous cache. |
354
- | Stream hangs or HTTP/2 errors | The provider keeps Cursor's Run open across OpenCode tool calls, rotates aged shared connections, resumes transient interruptions from the latest eligible Cursor checkpoint, and falls back to a fresh-history rebase only before stateful output when no checkpoint exists. Repeated interruption is surfaced as an error instead of a false successful stop; retry the turn after checking connectivity. Behind an HTTP proxy, set `HTTPS_PROXY` (and `NO_PROXY` as needed): unary `fetch` and the Run HTTP/2 CONNECT tunnel both honor them. With debug logging enabled, look for `Run interrupted`, `resuming … checkpoint`, `rebasing fresh Run`, or `h2 connect via HTTPS proxy`. Restart OpenCode after rebuilding a local `file://` install. |
355
- | No response / silent 200 + close | HTTP 200 alone is not a successful agent turn: the provider now requires Cursor's explicit `turn_ended`, captures HTTP/2 trailers/GOAWAY, and recovers once from bare EOF. The Run host still comes from in-memory `GetServerConfig` resolution; set `CURSOR_PROVIDER_DEBUG=1` to confirm the host and termination reason. |
530
+ | Stream hangs or HTTP/2 errors | The provider keeps Cursor's Run open across OpenCode tool calls, rotates aged shared connections, resumes transient interruptions from the latest eligible Cursor checkpoint, and falls back to a fresh-history rebase only before stateful output when no checkpoint exists. A transient interruption after only text or reasoning, without a checkpoint from that turn, is left to OpenCode to retry. Completed tools remain replay barriers across host steps; credential renewal retains the latest eligible checkpoint. Repeated resumes without progress end as an error. Behind an HTTP proxy, set `HTTPS_PROXY` (and `NO_PROXY` as needed): unary `fetch` and the Run HTTP/2 CONNECT tunnel both honor them. With debug logging enabled, look for `Run interrupted`, `resuming … checkpoint`, `rebasing fresh Run`, or `h2 connect via HTTPS proxy`. Restart OpenCode after rebuilding a local `file://` install. |
531
+ | No response / silent 200 + close | HTTP 200 alone is not a successful agent turn: the provider now requires Cursor's explicit `turn_ended`, captures HTTP/2 trailers/GOAWAY, and retries bare EOF within the transport retry budget. The Run host still comes from in-memory `GetServerConfig` resolution; set `CURSOR_PROVIDER_DEBUG=1` to confirm the host and termination reason. |
356
532
  | Visible `<shell_metadata>` timeout text | Rebuild and restart a local install. Cursor's shell timeout is carried on its exec request; the provider now removes OpenCode's internal timeout envelope before it is rendered or stored, then returns Cursor's typed timeout or background-handoff event instead of treating the text as successful stdout. |
357
533
  | `Unsupported Cursor exec variant …` | The error names the canonical Cursor CLI request field, its expected result field, and this provider's handling classification. Known `handling=unsupported` variants (Cursor-native capabilities without a safe OpenCode AI SDK bridge) are now soft-denied on the wire with a populated typed error/`rejected`/`allowlisted:false` result and the Run continues — look for `exec: SOFT-DENIED …` in the debug log and a `Workspace root: …` suffix in the deny text — while `unknown request field` still hard-fails as protocol drift; `handling=opencode-tool` or `provider-control` indicates a provider decoder/dispatch regression. Enable the debug log and report the full named error. |
358
- | Need wire-level logs | Set `CURSOR_PROVIDER_DEBUG=1` (optional `CURSOR_PROVIDER_DEBUG_FILE`; the default is `debug-<pid>.log` under `$TMPDIR/cursor-provider-logs-<uid>/`) and reproduce the issue. Every completed turn emits a `turn usage validation: status=ok|mismatch` line confirming token-accounting integrity and a `cache diagnosis:` line showing warm/cold checkpoint continuity, raw read/write/uncached tokens, cache-read coverage of the prior context, context/category deltas, stable RequestContext hashes, and Run step/tool counts. Checkpoint lines include Cursor's current category totals. Cursor does not expose per-model-call cache splits, so the diagnostic labels that value unavailable instead of inferring it from aggregate `TurnEnded` counters. |
359
- | OpenCode token totals stay stuck until the turn ends | OpenCode TUI/GUI display the last assistant message with non-zero output, they do not sum occupancy. Tool-call steps emit Cursor's last-known checkpoint occupancy (with `output > 0` so the footer accepts it) and `providerMetadata.copilot.totalNanoAiu: 0` so those snapshots are not billed. The billed occupancy is emitted once at `TurnEnded`. If a checkpoint has not arrived yet, the previous turn's occupancy stays visible as stale. With no known snapshot, usage stays zero instead of mislabeling estimates or `TurnEnded` as context. Raw request counters remain in `providerMetadata.cursor`. |
360
- | Context percentage jumps after a long tool turn or compaction | Cursor reports its authoritative context snapshot in checkpoint `token_details`. The provider makes AI SDK input + output equal `usedTokens`, proportionally partitions input into cached/uncached subsets, and exposes `maxTokens`, percentage, and category breakdown under `providerMetadata.cursor.context`. Mid-turn tool steps publish that occupancy as a display-only snapshot; compaction naturally replaces the prior snapshot with Cursor's smaller total. |
361
- | Cache **write** tokens always show as `0` | Cursor's `TurnEnded` protobuf includes `cache_write` (field 4), but captured Runs set it to `0` even when `cache_read` is large. The provider forwards the request-local value when Cursor supplies one; it cannot infer a missing write count. With debug logging, confirm `turn_ended raw wire fields: … f4:wt0=0`. |
534
+ | Need wire-level logs | Set `CURSOR_PROVIDER_DEBUG=1` (optional `CURSOR_PROVIDER_DEBUG_FILE`; the default is `debug-<pid>.log` under `$TMPDIR/cursor-provider-logs-<uid>/`) and reproduce the issue. Every completed turn emits a `turn usage validation: status=ok\|mismatch` line confirming token-accounting integrity and a `cache diagnosis:` line showing warm/cold checkpoint continuity, raw read/write/uncached tokens, cache-read coverage of the prior context, context/category deltas, stable RequestContext hashes, and Run step/tool counts. Checkpoint lines include Cursor's current category totals. Cursor does not expose per-model-call cache splits, so the diagnostic labels that value unavailable instead of inferring it from aggregate `TurnEnded` counters. |
535
+ | OpenCode token totals stay stuck until the turn ends | OpenCode TUI/GUI display the last assistant message with non-zero output, they do not sum occupancy. Tool-call steps emit Cursor's last-known checkpoint occupancy (with `output > 0` so the footer accepts it), billed as an estimate of one model call. At `TurnEnded` the session cost is reconciled toward the aggregate counters at catalog rates, within the limits below. If a checkpoint has not arrived yet, the previous turn's occupancy stays visible as stale. With no known snapshot, usage stays zero instead of mislabeling estimates or `TurnEnded` as context. Raw request counters remain in `providerMetadata.cursor`. |
536
+ | Context percentage jumps after a long tool turn or compaction | Cursor reports its authoritative context snapshot in checkpoint `token_details`. The provider makes AI SDK input + output equal `usedTokens`, shapes the input/cache partition for cost reporting, and exposes `maxTokens`, percentage, and category breakdown under `providerMetadata.cursor.context`. Mid-turn tool steps publish that occupancy with an estimated cost; compaction naturally replaces the prior snapshot with Cursor's smaller total. |
537
+ | Cache **write** tokens differ from Cursor’s raw counters | Standard input/cache counters are shaped for host cost reporting and may include synthetic cache-write tokens. Exact request counters remain in `providerMetadata.cursor.*Raw`. Captured Runs can report `cache_write=0`; this does not imply that the synthetic host split is a measured cache split. |
362
538
 
363
539
  ## Security
364
540
 
@@ -366,29 +542,39 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
366
542
 
367
543
  ## Known limitations
368
544
 
545
+ - **Native planning calls and handoffs** — CreatePlan and SwitchMode use their already supplied native function definitions, never dynamic discovery or CallDynamicTool. Where an approved switch resumes in the host's plan agent after the current turn, that turn ends before any further tool call; the following plan turn records the plan. Debug logs include server-side native discovery errors as well as MCP invocation errors, without replaying either as host execution.
369
546
  - **Personal use / ToS** — this provider speaks Cursor’s private agent protocol (CLI-shaped client identity). Use only with an account you own; Cursor may change or restrict the API without notice.
370
- - **Cursor's checkpoint controls displayed totals** — live continuation captures show `TurnEnded` counters can decrease between turns and can represent aggregate model work rather than final context occupancy. When a checkpoint contains `ConversationStateStructure.token_details`, the provider makes AI SDK input + output equal Cursor's `used_tokens`; at `TurnEnded`, output/reasoning remain based on `TurnEnded`, while uncached/cache-read/cache-write input are proportionally normalized to the remaining context input. OpenCode TUI/GUI replace each assistant message's tokens (TUI requires `tokens.output > 0`) and add cost per step, so mid-turn tool boundaries emit the last-known occupancy snapshot rather than a delta or a billed zero. Those snapshots set `output=1` so the footer updates and `providerMetadata.copilot.totalNanoAiu: 0` so OpenCode's getUsage reports $0 instead of charging occupancy as a new prompt. A Run without fresh token details retains the previous checkpoint snapshot and marks `providerMetadata.cursor.context` as `source: "checkpoint-previous-turn", stale: true`. If no checkpoint has ever supplied token details, standard usage is zero and context metadata is absent, so `TurnEnded` is never misrepresented as occupancy; its exact values remain under `providerMetadata.cursor.*Raw`. The full known snapshot includes `usedTokens`, `maxTokens`, percentage, category breakdown, source, and staleness. Cursor consistently sends `cache_write=0` in captured Runs, so no alternate write count is available.
371
- - **`request_context` from OpenCode** — each Run sends Cursor `RequestContext` with OpenCode's system prompt (its `AGENTS.md` / `instructions`, skill list, MCP instructions) plus the provider's interaction guidance as one always-apply rule, workspace env, git, layout, host-advertised `task`/`subagent` names, names-only tool overlay, and plugin lines. Cursor does not follow a client-seeded `system` message or an untyped rule, so the always-apply (`global`) rule is the channel that makes `AGENTS.md` effective; the provider does not read instruction or skill files itself. Merged `opencode.json` is still read for MCP server ids, plugin lists, and interaction guidance. Expensive workspace state (environment, repository, git, and layout) is frozen per Cursor conversation and persisted for restart recovery. Tools/MCP names, host subagents, and plugin metadata are rediscovered each Run then epoch-held (equal ids keep frozen bytes; new ids append); unchanged overlays reuse byte-identical encoded context. Concurrent first Runs share one base build, and compaction/reset clears it. Its canonical root is also used by the [injected system guidance](#injected-system-guidance). Cursor-only cloud/sandbox marketplace surfaces are omitted. `env.workspace_paths` / `process_working_directory` stay on the real git workspace; Cursor's metadata root (`project_folder`, MCP `workspace_project_dir`, terminals/transcripts) is advertised under `<host-cache>/projects/<slug>/` (default `~/.cache/opencode/projects/…`) so dumps like `agent-tools/` do not land in the repo. OpenCode remains the permission authority: its coarse allow/ask/deny configuration is not fabricated into Cursor's unrelated allow/block instruction-list messages.
372
- - **Configured MCP tools keep their upstream server id** — OpenCode builtins and plugin/custom tools are advertised under a synthetic `opencode` MCP server. Tools whose flattened name matches an MCP server in merged `opencode.json` configuration (`github_create_pull_request`, …) are grouped into that server's `mcp_descriptors` / `provider_identifier` (`github`, …). Unknown underscore-containing names stay under `opencode` rather than being guessed incorrectly. Cursor's MCP-state exec probe is answered from the same advertised descriptors before the actual tool request, using the full canonical tool-definition identity required by native `get_mcp_tools` / GetDynamicTools; exec still reconstructs the full OpenCode tool id. That discovery call is Cursor-native (not an OpenCode catalog tool). Oversized catalogs spill through `write_args` into `agent-tools/<uuid>.txt`; `WriteSuccess.path` echoes the requested path so the model receives a real `filePath` instead of an empty spill.
373
- - **Display completions are notifications, not execution requests** — Cursor `tool_call_*` frames use a typed `ToolCall` oneof. The provider decodes them for diagnostics but only mirrors finalized todo/plan state (`update_todos_tool_call` → `todowrite`, `read_todos_tool_call` → `todoread`, `create_plan_tool_call` → `todowrite`); the completed payload already contains the authoritative final list. Interactive, data-returning, and side-effecting completions are never replayed as new tools because their result could not be returned to Cursor. Exec-backed native subagent/Task and Pi read/bash/edit/write/grep/find/ls calls use their typed request/result fields instead. Unknown display variants are logged. All 37 Cursor CLI exec request/result pairs are inventoried by field and name. Known-but-unsupported variants are soft-denied on the held Run with a populated typed error/`rejected`/`allowlisted:false` result (or `throw` when the CLI has no typed error shape) so the turn continues; unknown future request fields still fail the Run explicitly rather than receiving a guessed response that could deadlock it.
547
+ - **Cursor's checkpoint controls displayed totals** — AI SDK input + output equal the latest checkpoint's `used_tokens`. Tool boundaries and the final finish use `output=1`, `reasoning=0`, and the remaining occupancy as input/cache, so OpenCode's footer updates without treating whole-Run generation counters as per-step speed. The input/cache split is synthetic for cost reporting; exact aggregate request counters remain under `providerMetadata.cursor.*Raw`. A retained previous checkpoint is marked `source: "checkpoint-previous-turn", stale: true`; without any snapshot standard usage stays zero and context metadata is absent. Compaction replaces occupancy with the smaller checkpoint total.
548
+ - **Cost reporting has bounded settlement** — tool steps estimate one model call from occupancy and the previous step's cached prefix. A final `TurnEnded` replaces that Run's estimates with its aggregate counters priced at published catalog rates, then shapes the final step toward the outstanding balance. If the Run closes before those counters arrive, its charges remain unconfirmed estimates (`sessionEstimatedUsd`) and never become credits against later Runs. OpenCode 2 prices that split; OpenCode 1.x receives the same amount in `providerMetadata.copilot.totalNanoAiu`. Whole-token rounding, a positive minimum price, and the snapshot's maximum price can prevent exact settlement. Negative balances cannot refund earlier charges; positive or negative remainders carry to later priced turn-ends and may persist. Missing occupancy or unpublished rates (Auto) bill zero and cannot settle an earlier remainder. The ledger is shared by package copies in one process, holds at most 1,024 recently written sessions, and resets on process restart. User-configured host rate overrides are not visible to the provider. This is catalog-priced accounting, not a Cursor invoice: aggregate counters do not expose per-call prompt sizes or tier choices, so internal compaction or calls crossing pricing tiers cannot be priced exactly. The tier estimate uses final context capped at aggregate input, so output alone cannot select a higher input tier.
549
+ - **`request_context` from OpenCode** — each Run sends Cursor `RequestContext` with OpenCode's system prompt (its `AGENTS.md` / `instructions`, skill list, MCP instructions) plus the provider's interaction guidance as one always-apply rule, workspace env, git, layout, host-advertised `task`/`subagent` names, names-only tool overlay, and plugin lines. Cursor does not follow a client-seeded `system` message or an untyped rule, so the always-apply (`global`) rule is the channel that makes `AGENTS.md` effective; the provider does not read instruction or skill files itself. Merged `opencode.json` is still read for MCP server ids, plugin lists, and interaction guidance. Expensive workspace state (environment, repository, git, and layout) is frozen per Cursor conversation and persisted for restart recovery. Tools/MCP names, host subagents, and plugin metadata are rediscovered each Run then epoch-held (equal ids keep frozen bytes; new ids append); unchanged overlays reuse byte-identical encoded context. Concurrent first Runs share one base build. Compaction transfers the stable workspace base to the new conversation and refreshes its capability overlay. Its canonical root is also used by the [injected system guidance](#injected-system-guidance). Cursor-only cloud/sandbox marketplace surfaces are omitted. `env.workspace_paths` / `process_working_directory` stay on the real git workspace; Cursor's metadata root (`project_folder`, MCP `workspace_project_dir`, and managed runtime folders) is advertised under `<host-cache>/projects/<slug>/` (using OpenCode's native cache directory by default) so dumps like `agent-tools/` do not land in the repo. OpenCode remains the permission authority: its coarse allow/ask/deny configuration is not fabricated into Cursor's unrelated allow/block instruction-list messages.
550
+ - **Configured MCP tools keep their upstream server id** — OpenCode builtins and plugin/custom tools are advertised under a synthetic `opencode` MCP server. Tools whose flattened name matches an MCP server in merged `opencode.json` configuration (`github_create_pull_request`, …) are grouped under that server's `provider_identifier` in the dynamic catalog (`github`, …). Unknown underscore-containing names stay under `opencode` rather than being guessed incorrectly. Cursor's MCP-state exec probe is answered from the same advertised descriptors before the actual tool request, using the full canonical tool-definition identity required by native `get_mcp_tools` / GetDynamicTools; exec still reconstructs the full OpenCode tool id. That discovery call is Cursor-native (not an OpenCode catalog tool). Oversized catalogs spill through `write_args` into `agent-tools/<uuid>.txt`; `WriteSuccess.path` echoes the requested path so the model receives a real `filePath` instead of an empty spill.
551
+ - **Display completions are notifications, not execution requests** — Cursor `tool_call_*` frames use a typed `ToolCall` oneof. The provider decodes them for diagnostics but only mirrors finalized todo/plan state (`update_todos_tool_call` → `todowrite`, `read_todos_tool_call` → `todoread`, `create_plan_tool_call` → `todowrite`); the completed payload already contains the authoritative final list. Interactive, data-returning, and side-effecting completions are never replayed as new tools because their result could not be returned to Cursor. Exec-backed native subagent/Task and Pi read/bash/edit/write/grep/find/ls calls use their typed request/result fields instead. Unknown display variants are logged. Cursor CLI exec request/result pairs are inventoried by field and name in [`src/protocol/exec-variants.ts`](src/protocol/exec-variants.ts). Known-but-unsupported variants are soft-denied on the held Run with a populated typed error/`rejected`/`allowlisted:false` result (or `throw` when the CLI has no typed error shape) so the turn continues; unknown future request fields still fail the Run explicitly rather than receiving a guessed response that could deadlock it.
374
552
  - **Progress-only fragments can get one continuation** — if a real tool-enabled turn ends with a short progress fragment ("Checking the workspace", "Looking into it now") and no host tool call, the provider opens one follow-up Run with a synthetic user nudge so the model can call a listed tool or finish the answer. Complete answers that merely start with those verbs are not continued. Title/compaction turns (`allowTools=false`) never continue. If the continuation fails, the original `turn_ended` is still reported as a successful stop.
375
553
  - **Tool availability is per OpenCode agent** — Cursor can request native capabilities such as Task even when a child or restricted OpenCode agent did not advertise the corresponding host tool. The provider prompts Cursor with the exact current catalog and checks every decoded host-tool exec request against it. An unavailable request is answered on Cursor's correlated typed result channel and is never emitted as OpenCode's `invalid` tool.
376
554
  - **Host web tools use collision-safe aliases** — Cursor sees `custom_websearch` / `custom_webfetch`, and the held Run maps each alias back to an executable OpenCode tool with its schema, permission check, and correlated result intact. OpenCode 2 ships `websearch` as a direct, permission-gated catalog tool; this plugin supplies it an Exa provider through `ctx.websearch.transform` and aliases the host tool for Cursor. It does not add a second direct fallback because OpenCode 2's public plugin tool context cannot raise the host permission prompt. Classic 1.x still registers `custom_websearch` through its permission-aware tool API to avoid OpenCode's reserved `websearch` id filter. Exact host `webfetch` is translated the same way. Cursor's UI-bound native web interactions stay disabled because their approval replies cannot carry OpenCode tool results.
377
555
  - **MCP resource tools (`list_mcp_resources` / `read_mcp_resource`) also use collision-safe aliases, and Cursor's native resource exec is always answered** — OpenCode's resource tools (advertised only when a connected MCP server declares `capabilities.resources`) share bare names with two of Cursor's own native tool-call variants. Left unaliased, Cursor's client can route a call onto its native exec channel instead of the ordinary MCP tool path, which this provider does not translate — confirmed live: reading a resource was dispatched natively and killed the Run before this fix. The provider now aliases both names to `custom_list_mcp_resources` / `custom_read_mcp_resource` (same mechanism as web search/fetch) so normal calls execute through OpenCode's tools, and separately answers Cursor's native resource-exec fields directly on the protocol floor — an empty list, or a "server not found" error for read — so an unsolicited native request from Cursor can never fail the Run. Downloads (`download_path`) have no OpenCode equivalent and are always refused; binary resource content is subject to the same [50 KB / attachment-type limits](#known-limitations) as other tool results.
378
- - **Background shells are non-interactive** — Cursor's native background-shell spawn and soft-background shell-stream timeouts are bridged through OpenCode's foreground-only `bash` tool. With bash/zsh, the classic plugin keeps the original permission/UI command and executes the wrapper through `shell.env` (`BASH_ENV` / `ZDOTDIR`); OpenCode's non-interactive sh/dash argv ignores those startup variables, so the plugin uses a short `exec /bin/sh '<wrapper-file>'` command backed by the same private wrapper. Native background-spawn requests also carry a self-contained marker-producing fallback when the classic hooks are absent. Spawn/soft-bg wrappers detach with `nohup`, redirect output under `${TMPDIR:-/tmp}/cursor-opencode-{bg,shell}.*`, and return the real PID (or typed timeout/exit) to Cursor. Private markers and OpenCode's `<shell_metadata>` envelope are stripped before storage/render, with a short still-running / started / timed-out status line left for the bash bubble. Requests that require `write_shell_stdin` are rejected explicitly because OpenCode does not expose an interactive background-process lifecycle through its AI SDK tool interface. The POSIX wrap path is not implemented for native Windows PowerShell/`cmd`.
556
+ - **Background shells are non-interactive** — Cursor's native background-shell spawn and soft-background shell-stream timeouts are bridged through the advertised OS shell (`bash` on 1.x, `shell` on 2.0). Native OpenCode 2 `background` calls keep the host's own job ownership and completion notices. With bash/zsh, the classic plugin keeps the original permission/UI command and executes the wrapper through `shell.env` (`BASH_ENV` / `ZDOTDIR`); OpenCode's non-interactive sh/dash argv ignores those startup variables, so the plugin uses a short `exec /bin/sh '<wrapper-file>'` command backed by the same private wrapper. Native background-spawn requests also carry a self-contained marker-producing fallback when the classic hooks are absent. Managed spawn/soft-background runners detach with `nohup`, write `terminals/<shell-id>.txt` under the pinned provider project root, and return a separate shell ID and supervisor PID (or typed timeout/exit) to Cursor; without managed storage the POSIX wrappers redirect output under `${TMPDIR:-/tmp}/cursor-opencode-{bg,shell}.*` and return the command's PID. Native polling and canonical metadata reads are answered directly by the provider. Private markers and OpenCode's `<shell_metadata>` envelope are stripped before storage/render, with a short still-running / started / timed-out status line left for the bash bubble. Requests that require `write_shell_stdin` are rejected explicitly because OpenCode does not expose an interactive background-process lifecycle through its AI SDK tool interface. The POSIX wrap path is not implemented for native Windows PowerShell/`cmd`.
379
557
  - **Cursor-native AskQuestion reaches you through OpenCode's question tool** — when Cursor asks a clarifying question, the provider translates the interaction into the host `question` tool, so the prompt appears in OpenCode with its real options and your answer is mapped back to Cursor's option ids (an answer you type yourself becomes Cursor's freeform text). Questions Cursor marks as non-blocking are acknowledged immediately and answered later, matching Cursor CLI. If the current agent does not offer `question` — most subagents deny it, and compaction turns have no tools — the request is declined with that reason and the model asks in its reply instead.
380
- - **Cursor-native SwitchMode follows each OpenCode generation's planning model** — on OpenCode 1.x, advertised `plan_enter` / `plan_exit` tools remain authoritative and Cursor waits for their approve/reject result. OpenCode 2.0 replaced those tools with vendor-maintained primary `plan` / `build` agents, so its plugin applies an approved Cursor mode change through `session.switchAgent` after the owning Run is terminal; leaving plan still uses the advertised `question` gate when no `plan_exit` tool exists. Direct host UI agent switches are also observed. Any real host-agent or stable system-prompt change rotates and reseeds the Cursor conversation instead of resuming an incompatible checkpoint, while title/generation lifecycle prompts remain ephemeral and do not evict the primary cache. Provider-owned Cursor reminders remain the fallback when a host cannot switch agents.
381
- - **Cursor image generation saves through OpenCode's permission where the host API exposes it** — Cursor generates the image on its servers and then writes it like any other file. On the classic plugin/OCP surfaces, the provider holds raw bytes and asks for the same permissions as a normal write (`external_directory`, then `edit`) before saving byte-for-byte. OpenCode 2.0's public plugin `ToolContext` does not expose a permission-request method, so its entrypoint deliberately does not advertise `cursor_image_save`; generation and binary writes are refused before staging instead of bypassing permissions or exposing an always-failing tool. By default supported-host images land in the Cursor project folder under `assets/`, kept out of the git tree as agent artifacts. Editing an existing image with `reference_image_paths` is not supported yet. The classic implementation is adapted from [`jkalasas/opencode-antigravity-image`](https://github.com/jkalasas/opencode-antigravity-image) (MIT) by jkalasas.
382
- - **Switching models mid-session reseeds Cursor instead of resuming a stale checkpoint** — a Cursor conversation only contains what Cursor itself produced. When the host's latest assistant turn in a session came from another model (another provider or a local model), the provider starts a fresh Cursor conversation seeded with the full host history, tool results included, rather than resuming the old checkpoint that would hide the other model's work (and has been observed to end Runs with `Cursor API error (code=internal)`). If that history would fill more than 80% of the target model's context, the provider reports a context overflow before opening a Run so the host compacts first.
383
- - **Cursor-native CreatePlan writes a host plan file, then asks before executing it** — when Cursor raises `create_plan_request_query`, the provider writes plain markdown to your host's own plans directory (`<hostGlobalDataDir>/plans/`, never your repository and never Cursor's `~/.cursor/plans`), with no Cursor YAML frontmatter. Writing the plan does not ask; running it does. If you are in plan mode, the provider prints the plan into the conversation — Cursor sends it over a side channel that would otherwise never display it — and then asks *"Plan at &lt;path&gt; is complete. Would you like to switch to the build agent and start implementing?"* — the same prompt OpenCode's own `plan_exit` uses — and keeps Cursor waiting for your answer. Answer **Yes** and the plan is reported as approved and implementation begins; answer **No** (or dismiss it) and Cursor is told the plan was not accepted, so the model refines it and proposes again instead of starting work. Hosts that advertise their own plan-staging tool run their native review UI instead, and it reports the same approve/refine outcome. Empty args still get the CLI empty-`plan_uri` success ack. Display `create_plan_tool_call` continues to mirror into `todowrite` separately.
558
+ - **Cursor-native SwitchMode follows each OpenCode generation's planning model** — on OpenCode 1.x, advertised `plan_enter` / `plan_exit` tools remain authoritative and Cursor waits for their approve/reject result. OpenCode 1.x without `plan_enter` (current releases advertise only `plan_exit`, behind `OPENCODE_EXPERIMENTAL_PLAN_MODE`) is switched into its own `plan` primary agent once the owning Run ends, through a synthetic `session.promptAsync` turn for that agent (only for sessions running one of the primary agents read from `app.agents()`); a CreatePlan raised before that switch is deferred to the plan-agent turn. Inside the `plan` agent, CreatePlan writes the plan to the session's plan file (OpenCode's `Session.plan` location, or the structural path bridge's `planFile`) and runs the `plan_exit` review; OpenCode moving the session to its build agent is the approval. Cursor's own mode follows the OpenCode agent on every turn (`UserMessage.mode`, as Cursor CLI sends it): choosing `plan` in the agent picker puts Cursor in plan mode, and leaving it ends Cursor plan mode. OpenCode 2.0 replaced those tools with vendor-maintained primary `plan` / `build` agents, so its plugin applies an approved Cursor mode change through `session.switchAgent` after the owning Run is terminal; without a `plan_exit` tool, the plan is approved through CreatePlan's `question` prompt (below), and a Cursor SwitchMode out of the host `plan` agent is pointed there. Direct host UI agent switches are also observed. Host-agent and system-prompt changes keep the sticky Cursor conversation and frozen baseline; changed instructions arrive chronologically on the next user turn. Title/generation lifecycle prompts remain ephemeral and do not evict the primary cache. Provider-owned Cursor reminders remain the fallback when a host cannot switch agents.
559
+ - **Cursor image generation saves through OpenCode's permission where the host API exposes it** — Cursor generates the image on its servers and then writes it like any other file. The provider holds raw bytes and commits them through handle-only `cursor_image_save`. On the classic plugin/OCP surfaces it asks for the same permissions as a normal write (`external_directory`, then `edit`) before saving byte-for-byte. OpenCode 2.0's public plugin `ToolContext` has no permission-request method, so that entrypoint still advertises the tool with catalog `permission: "edit"` and writes after containment. By default images land in the Cursor project folder under `assets/`, kept out of the git tree as agent artifacts. GenerateImage `reference_image_paths` use the same held-Run `ReadSuccess.data` path as an ordinary image `read` (no CLI resize; large refs may still hit the 20 MiB decode budget). The classic implementation is adapted from [`jkalasas/opencode-antigravity-image`](https://github.com/jkalasas/opencode-antigravity-image) (MIT) by jkalasas.
560
+ - **Returning after another provider answered reseeds foreign history** — a Cursor conversation only contains what Cursor itself produced. When the host's latest assistant turn in a session came from another provider or a local model, the provider starts a fresh Cursor conversation seeded with the full host history, tool results included, rather than resuming the old checkpoint that would hide the other model's work (and has been observed to end Runs with `Cursor API error (code=internal)`). If that history would fill more than 80% of the target model's context, the provider reports a context overflow before opening a Run so the host compacts first. Switching directly between Cursor models keeps the same conversation and checkpoint.
561
+ - **Cursor-native CreatePlan writes a host plan file, and executing it always needs the user's approval** — when Cursor raises `create_plan_request_query`, the provider writes plain markdown where OpenCode itself keeps plans (never Cursor's `~/.cursor/plans`), with no Cursor YAML frontmatter: on OpenCode 1.x the session's `Session.plan` file (`<worktree>/.opencode/plans/<created>-<slug>.md` in a git project, else `<data>/plans/`); on OpenCode 2.0 its Plan directory (`~/.opencode/plan/`). A structural path bridge, when installed, supplies the host's own location instead. Writing never asks; executing always does, through the first channel the host offers: a host plan-stage tool runs its native review; under the host `plan` agent with `plan_exit`, CreatePlan runs that review and the session moving to the build agent is the approval; otherwise, in plan mode, the plan is shown in the transcript and the advertised `question` tool asks whether to switch to the build agent and start implementing (Yes switches and continues; anything else keeps planning). With none of these the plan is recorded and the user switches to the build agent when they want it implemented. Empty args still get the CLI empty-`plan_uri` success ack. Display `create_plan_tool_call` continues to mirror into `todowrite` separately.
384
562
  - **Other Cursor-native interaction queries remain headless** — remaining UI/approval *queries* (as distinct from display tool calls and the AskQuestion / SwitchMode / CreatePlan / GenerateImage bridges) still cannot be surfaced through the AI SDK provider interface. The normal system prompt redirects available web capabilities to executable host tools (`custom_websearch`, `custom_webfetch`); native web/PR/MCP/SCM requests are declined so they remain behind host tool permissions. Compaction prompts are unchanged. Unknown future interaction variants fail the turn explicitly instead of hanging the Run stream.
385
- - **Compaction resets Cursor conversation state without resetting stable workspace context** — the classic plugin marks OpenCode's `compaction` agent explicitly. On those turns the provider mints an isolated Cursor `conversation_id`, drops the prior checkpoint + KV blobs, preserves real tool outputs as OpenCode-host observations in the seed history, and re-advertises the session's last tool catalog while refusing execution during the summary itself. The first normal turn then rebases once more onto a fresh conversation seeded with OpenCode's compacted prompt and normal system instructions, so the summary-agent checkpoint cannot suppress later tool calls. Across both id rotations, the frozen workspace/rules/environment base and its prior encoded-context comparison seed are transferred instead of rebuilt; tools, skills, agents, plugins, and MCP state are still rediscovered, and the complete context is reused only when its bytes remain identical. Ordinary no-tool / `toolChoice:none` calls do not reset conversation state. Debug logs include system-prompt and encoded RequestContext hashes for cache-prefix verification.
386
- - **Conversation bindings survive provider restarts** — after each successful `TurnEnded`, the OpenCode-session binding, latest checkpoint, checkpoint-reachable KV blob graph, frozen request-context base, last real tool catalog, host agent, stable system-prompt hash, and pending post-compaction rebase marker are atomically replaced in that session's private `<host-cache>/cursor-conversations/*.pb.gz` snapshot. The protobuf stores checkpoints, blob ids/data, and request context as bytes without JSON/base64 expansion, then gzip compresses the complete record. A checkpoint alone is insufficient: Cursor requests its referenced hashes through `get_blob` when the next Run starts. Reachability follows Cursor CLI's export traversal (turns, messages, steps, images, summaries, todos, prompts, and nested subagents), pruning superseded blobs; decode failure or a missing referenced hash conservatively retains the complete blob set. The stable OpenCode session id selects the file; Cursor conversation ids may rotate during compaction, agent switches, or prompt changes and remain inside the snapshot. Independent files prevent concurrent OpenCode processes from losing unrelated sessions by replacing a shared aggregate cache. Startup restores records updated within 24 hours and safely flushes older ones without deleting a concurrently published fresh replacement. Experimental legacy JSON snapshots are discarded rather than migrated. Debug logs report hydration as `restored`, `missing`, `invalid`, or `expired`, plus the compressed and protobuf snapshot sizes. The restored catalog is a fallback only for lifecycle turns such as compaction; an ordinary restricted/no-tool turn never resurrects it. Process-global bindings and prompt/catalog state still use a 256-session LRU bound; memory eviction drops the live checkpoint, blobs, and frozen context, which can be rehydrated from the restart cache on later use.
387
- - **Oversized / incomplete checkpoint graphs warn but do not remint** — before reusing a checkpoint, the provider measures its reachable KV graph. State above 100 MiB (Cursor CLI export/transfer budget) or an incomplete graph is logged (`action=warm-reuse`) and the sticky conversation is kept, matching CLI soft-reuse. KV, request-context, MCP-state, interaction, and heartbeat writes are ordered per Run and await HTTP/2 drain, so a large blob reply cannot create an unbounded queue later misreported as heartbeat backpressure. Debug logs distinguish checkpoint-envelope bytes, reachable blob bytes, seed size, and encoded Run-request estimate; raw blob bytes are never presented as tokens.
563
+ - **Compaction resets Cursor conversation state without resetting stable workspace context** — the classic plugin marks OpenCode's `compaction` agent explicitly. On those turns the provider mints an isolated Cursor `conversation_id`, drops the prior checkpoint + KV blobs, preserves real tool outputs as OpenCode-host observations in the seed history, and refuses execution during the summary itself. A host-empty catalog stays empty (including classic compaction); OpenCode 2 compaction can carry a nonempty catalog, whose epoch advertisement is retained. The first normal turn then rebases once more onto a fresh conversation seeded with OpenCode's compacted prompt and normal system instructions, so the summary-agent checkpoint cannot suppress later tool calls. Across both id rotations, the stable workspace base and prior encoded-context comparison seed transfer. Each new epoch admits its own system-instructions baseline; tools, skills, agents, plugins, and MCP state are still rediscovered, and the complete context is reused only when its bytes remain identical. Ordinary no-tool / `toolChoice:none` calls do not reset conversation state. Debug logs include system-prompt and encoded RequestContext hashes for cache-prefix verification.
564
+ - **Conversation bindings survive provider restarts** — after each successful `TurnEnded`, the OpenCode-session binding, latest checkpoint, checkpoint-reachable KV blob graph, frozen request-context base, last real tool catalog, host agent, stable system-prompt hash, pending post-compaction rebase marker, deferred host notes, turn provenance, and early-steer receipts are atomically replaced in that session's private `<host-cache>/cursor-conversations/*.pb.gz` snapshot. The protobuf stores checkpoints, blob ids/data, and request context as bytes without JSON/base64 expansion, then gzip compresses the complete record. A checkpoint alone is insufficient: Cursor requests its referenced hashes through `get_blob` when the next Run starts. Reachability follows Cursor CLI's export traversal (turns, messages, steps, images, summaries, todos, prompts, and nested subagents), pruning superseded blobs; decode failure or a missing referenced hash conservatively retains the complete blob set. The stable OpenCode session id selects the file; Cursor conversation ids may rotate during compaction or a recovery/history rebase and remain inside the snapshot; agent and prompt changes alone do not rotate them. Independent files prevent concurrent OpenCode processes from losing unrelated sessions by replacing a shared aggregate cache. On-demand hydration accepts records updated within 24 hours; independent maintenance reads only bounded metadata prefixes and preserves concurrently published replacement files. Payload/checksum validation runs on restoration, and filesystem modification times never determine retention. Experimental legacy JSON snapshots are discarded rather than migrated. Debug logs report hydration as `restored`, `missing`, `invalid`, or `expired`, plus the compressed and protobuf snapshot sizes. The restored catalog can retain epoch advertisement, but never grants execution permission: each turn uses its own permitted set, and a host-empty call still advertises `[]`. The recent-session binding index uses a 256-session LRU bound; eviction removes its lookup pointer while retaining checkpoint, blob, and frozen-context state for warm reuse.
565
+ - **Oversized / incomplete checkpoint graphs warn but do not remint** — before reusing a checkpoint, the provider measures its reachable KV graph. State above 100 MiB (Cursor CLI export/transfer budget) or an incomplete graph is logged (`action=warm-reuse`) and the sticky conversation is kept, matching CLI soft-reuse. KV, request-context, MCP-state, interaction, and heartbeat writes are ordered per Run and await HTTP/2 drain, so a large blob reply cannot create an unbounded queue later misreported as heartbeat backpressure. Uploads have no fixed drain deadline; stream termination and the read-idle watchdog bound failed connections. Stopping a turn cancels its initial, tool-result, or replacement upload, while the normal abort after a tool step keeps that Run open for its results. Debug logs distinguish checkpoint-envelope bytes, reachable blob bytes, seed size, and encoded Run-request estimate; raw blob bytes are never presented as tokens.
566
+ - **Edit rows can appear before the call is complete** — an announced edit opens a host tool row while Cursor streams its arguments. The eventual exec binds that row; if no call arrives, the provider closes it without asking the host to execute incomplete arguments ([early rows](src/early-tool-rows.ts)). An edit whose file read must first go through OpenCode's `read` (a file outside the workspace, or one the provider cannot read) opens no early row: that read is the call's first exec and would close the row as a failed edit. When the call runs as a host tool the row did not predict (a rewrite with unchanged content stays a `write`), the row completes with a note naming that call; OpenCode 2 does not allow a started row to change its tool name.
567
+ - **Large uploads share a connection without the default 10 MB bottleneck** — direct and proxied Run connections use a 256 MB session-memory limit. This prevents queued sibling requests, or one large Node upload on a slow uplink, from triggering the default `NGHTTP2_ENHANCE_YOUR_CALM` refusal. The limit remains finite. Cached connections hold the process alive only while a Run or health-check ping is active, so an idle connection does not prevent a headless process from exiting ([transport](src/transport/connect.ts)).
568
+ - **A quiet model or a running tool is still active** — every inbound frame, including heartbeats while a model composes tool arguments, renews Run liveness. Pending host execs receive client heartbeats; while tools run, the provider continues answering Cursor's KV requests and exec aborts. Open permission/question prompts and active descendant tools renew held Runs; a running tool can extend its lease up to four hours from its own start ([Run reader and heartbeats](src/language-model.ts), [leases](src/session.ts), [activity](src/activity.ts)).
569
+ - **Requests not yet checkpointed survive failed turns** — the next turn on the same checkpoint carries the missing request, including after a restart. Repeated retries do not stack copies of the same interruption note. A newer checkpoint, host Stop or rebuild from host history clears that carryover ([request tracking](src/lost-request.ts)).
388
570
  - **Interrupted Runs resume from checkpoints** — a remote EOF, Connect end-stream, or trailer error is never emitted as a successful `stop`. When the failed Run produced an eligible checkpoint, the provider opens a new RPC for the same conversation and sends that state with `ResumeAction`, so completed text and tool work are not replayed. Before any stateful output, an interruption without a checkpoint can still rebase from OpenCode history. Stateful interruptions without a checkpoint are surfaced because replay would be ambiguous; retry exhaustion remains explicit. A transport closure after `turn_ended` is treated as successful completion.
389
- - **Large reads are capped by OpenCode, and the cap is reported** — OpenCode 1.x `read` stops at `MAX_BYTES = 50 * 1024` and appends `(Output capped at 50 KB. Showing lines X-Y. Use offset=N to continue.)`. OpenCode 2 prints `Read file <path>, lines <start>-<end>` with `N: ` prefixes and, when the page ends before EOF, `[Output truncated. Continue reading with offset: N]` (50 KB or 2,000 lines); it also shortens each individual line after 2,000 characters. The provider strips either envelope before returning content to Cursor so the model cannot echo wrappers into a later write, but it re-states every partial result: a native read appends a `[Partial read: …]` marker after the content, an MCP read gets a separate notice content item, and a Pi read carries structured `PiReadExecSuccess.truncation`. Relative grep, glob, and directory entries are resolved against the workspace root. Shell stdout absolutizes only relative path tokens, not prose. Checkpointed turns, which do not resend the system prompt, get that same root on the user message. The structured `truncated` flag alone is not enough — verified against live `gpt-5.4-mini` and `grok-4.5` sessions, both of which asserted "highly confident" that partial content was the whole file until the textual marker was added. Deliberately paged reads (explicit `offset`/`limit`) are not marked unless the host also hits the byte cap or character-truncates a line. Whole-file writes and overwriting `Add File` patches that echo the marker are rejected before OpenCode executes them; targeted edits remain possible, including edits to source that quotes the warning text. Cursor's private legacy-edit read is handled separately as described above, so an edit can operate on a workspace file larger than 50 KB without turning the capped preview into a replacement. The cap is hardcoded in OpenCode's read tool — it is not the configurable `tool_output.max_bytes`, which governs `Truncate.Service` and not read's line accumulation — so ordinary model reads still require paging with `offset`.
571
+ - **Large reads are capped by OpenCode, and the cap is reported** — OpenCode 1.x `read` stops at `MAX_BYTES = 50 * 1024` and appends `(Output capped at 50 KB. Showing lines X-Y. Use offset=N to continue.)`. OpenCode 2 prints `Read file <path>, lines <start>-<end>` with `N: ` prefixes and, when the page ends before EOF, `[Output truncated. Continue reading with offset: N]` (50 KB or 2,000 lines); it also shortens each individual line after 2,000 characters. The provider strips either envelope before returning content to Cursor so the model cannot echo wrappers into a later write, but it re-states every partial result: a native read appends a `[Partial read: …]` marker after the content, an MCP read gets a separate notice content item, and a Pi read carries structured `PiReadExecSuccess.truncation`. Relative grep, glob, and directory entries are resolved against the workspace root. Shell stdout reaches Cursor verbatim, including whitespace around internal wrapper markers; failed streams retain partial output. Successful timeout-shaped stdout remains text. Checkpointed turns, which do not resend the system prompt, get that same root on the user message. The structured `truncated` flag alone is not enough — verified against live `gpt-5.4-mini` and `grok-4.5` sessions, both of which asserted "highly confident" that partial content was the whole file until the textual marker was added. Deliberately paged reads (explicit `offset`/`limit`) are not marked unless the host also hits the byte cap or character-truncates a line. Whole-file writes and overwriting `Add File` patches that echo the marker are rejected before OpenCode executes them; targeted edits remain possible, including edits to source that quotes the warning text. Cursor's private legacy-edit read is handled separately as described above, so an edit can operate on a workspace file larger than 50 KB without turning the capped preview into a replacement. The cap is hardcoded in OpenCode's read tool — it is not the configurable `tool_output.max_bytes`, which governs `Truncate.Service` and not read's line accumulation — so ordinary model reads still require paging with `offset`.
390
572
  - **No fallback models** — if Cursor’s `AvailableModels` API is unreachable and there is no local cache, the provider exposes no models.
391
573
 
574
+ ## Changelog
575
+
576
+ See [CHANGELOG.md](CHANGELOG.md) for user-facing release notes. New work lands under `[Unreleased]` until the next version tag.
577
+
392
578
  ## License
393
579
 
394
580
  MIT