cursor-opencode-provider 0.4.3 → 0.6.0

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 (71) hide show
  1. package/README.md +33 -23
  2. package/dist/context/agents.d.ts +1 -1
  3. package/dist/context/agents.js +1 -1
  4. package/dist/context/auth-store.js +2 -2
  5. package/dist/context/build.d.ts +11 -1
  6. package/dist/context/build.js +76 -25
  7. package/dist/context/frozen.d.ts +29 -0
  8. package/dist/context/frozen.js +168 -0
  9. package/dist/context/index.d.ts +3 -2
  10. package/dist/context/index.js +3 -2
  11. package/dist/context/paths.d.ts +34 -53
  12. package/dist/context/paths.js +54 -118
  13. package/dist/context/rules.d.ts +1 -0
  14. package/dist/context/rules.js +2 -2
  15. package/dist/image-save-tool.d.ts +19 -0
  16. package/dist/image-save-tool.js +25 -0
  17. package/dist/image-save.d.ts +58 -0
  18. package/dist/image-save.js +189 -0
  19. package/dist/image-staging.d.ts +59 -0
  20. package/dist/image-staging.js +91 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/language-model.d.ts +13 -11
  23. package/dist/language-model.js +957 -150
  24. package/dist/plan-execution-kickoff.d.ts +55 -0
  25. package/dist/plan-execution-kickoff.js +162 -0
  26. package/dist/plugin-opencode2.js +52 -3
  27. package/dist/plugin-v2.d.ts +11 -9
  28. package/dist/plugin-v2.js +6 -13
  29. package/dist/plugin.d.ts +9 -0
  30. package/dist/plugin.js +61 -5
  31. package/dist/pricing-data.d.ts +16 -4
  32. package/dist/pricing-data.js +16 -4
  33. package/dist/protocol/ask-question.d.ts +133 -0
  34. package/dist/protocol/ask-question.js +320 -0
  35. package/dist/protocol/blob-reachability.d.ts +15 -0
  36. package/dist/protocol/blob-reachability.js +140 -0
  37. package/dist/protocol/blob-store.d.ts +37 -0
  38. package/dist/protocol/blob-store.js +115 -0
  39. package/dist/protocol/conversation-bind.d.ts +13 -0
  40. package/dist/protocol/conversation-bind.js +27 -1
  41. package/dist/protocol/conversation-persistence.d.ts +32 -0
  42. package/dist/protocol/conversation-persistence.js +504 -0
  43. package/dist/protocol/conversation-state.d.ts +16 -0
  44. package/dist/protocol/conversation-state.js +62 -0
  45. package/dist/protocol/create-plan.d.ts +141 -0
  46. package/dist/protocol/create-plan.js +373 -0
  47. package/dist/protocol/generate-image.d.ts +54 -0
  48. package/dist/protocol/generate-image.js +101 -0
  49. package/dist/protocol/interactions.d.ts +110 -8
  50. package/dist/protocol/interactions.js +295 -35
  51. package/dist/protocol/kv.d.ts +1 -0
  52. package/dist/protocol/kv.js +2 -0
  53. package/dist/protocol/messages.d.ts +2 -0
  54. package/dist/protocol/messages.js +148 -8
  55. package/dist/protocol/request.d.ts +2 -0
  56. package/dist/protocol/request.js +2 -0
  57. package/dist/protocol/switch-mode.d.ts +178 -0
  58. package/dist/protocol/switch-mode.js +417 -0
  59. package/dist/protocol/token-details.d.ts +33 -0
  60. package/dist/protocol/token-details.js +155 -0
  61. package/dist/protocol/tool-call-bridge.js +10 -5
  62. package/dist/protocol/tools.d.ts +44 -11
  63. package/dist/protocol/tools.js +171 -41
  64. package/dist/session.d.ts +39 -11
  65. package/dist/shared.d.ts +3 -0
  66. package/dist/shared.js +3 -0
  67. package/dist/usage.d.ts +42 -37
  68. package/dist/usage.js +201 -70
  69. package/dist/web-search-tool.d.ts +22 -26
  70. package/dist/web-search-tool.js +25 -15
  71. package/package.json +7 -4
package/README.md CHANGED
@@ -126,7 +126,7 @@ For OpenCode builds that use the Effect/Promise **v2** plugin API (`plugins` fie
126
126
 
127
127
  Local clone equivalent: `"file:///absolute/path/to/cursor-opencode-provider/dist/plugin-v2.js"`.
128
128
 
129
- That entry registers the provider via `ctx.aisdk.sdk` / `ctx.aisdk.language`. Keep the classic `plugin` entry for auth.
129
+ That entry registers the provider via `ctx.aisdk.sdk` / `ctx.aisdk.language`. Keep the classic `plugin` entry for auth **and** plugin tools (notably `cursor_image_save` / `custom_websearch`): the 1.18 `/v2/promise` API has no `tool` domain, so a v2-only load silently disables Cursor image generation.
130
130
 
131
131
  ### OpenCode 2.0 beta (`opencode2`)
132
132
 
@@ -243,23 +243,21 @@ Choose the **cursor** provider, then one of:
243
243
  | **Cursor account (browser login)** | PKCE OAuth — opens cursor.com to sign in |
244
244
  | **API key** | Paste a key from [cursor.com/settings](https://cursor.com/settings) (`crsr_...`) |
245
245
 
246
- After login, the plugin fetches your available models and writes them to `<host-cache>/cursor-models.json` (default `~/.cache/opencode/`). 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.
246
+ 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.
247
247
 
248
248
  ### Paths (host cache)
249
249
 
250
250
  Model/version caches and Cursor project metadata live under a **host cache root**, resolved in this order:
251
251
 
252
- 1. Explicit `createCursor({ cacheDir })` / Effect v2 `Path.cache`
253
- 2. Optional `@opencode-compat/profile` `detect()` when it reports strong environment, binary, or package identity
254
- 3. Explicit `$MIMOCODE_HOME` / `KILO_CONFIG_DIR`, then the provider's host-named install path under `$XDG_CACHE_HOME`
255
- 4. Default `~/.cache/opencode/`
252
+ 1. Explicit `createCursor({ cacheDir })` / host API cache override
253
+ 2. A structural host path bridge installed before the provider loads
254
+ 3. Native OpenCode: `$XDG_CACHE_HOME/opencode`, otherwise `~/.cache/opencode`
256
255
 
257
- Config-directory presence is deliberately ignored: having MiMo or Kilo installed
258
- must not redirect a native OpenCode process into that host's cache.
256
+ The provider never detects or names fork hosts. Compatibility layers own host identity and may inject cache, data, and config roots through the structural bridge; without one, standalone OpenCode behavior is unchanged.
259
257
 
260
258
  | Kind | Default (OpenCode) | Notes |
261
259
  |------|--------------------|-------|
262
- | Model / version **cache** | `~/.cache/opencode/` | MiMo: `$MIMOCODE_HOME/cache` or `~/.cache/mimocode/`; Kilo: `~/.cache/kilo/` |
260
+ | Model, version, and 24-hour conversation **cache** | `~/.cache/opencode/` | A host bridge may replace the root |
263
261
  | Cursor **project metadata** (`agent-tools`, terminals, …) | `~/.cache/opencode/projects/<slug>/` | under `<host-cache>/projects/` |
264
262
  | OpenCode **auth** (`auth.json`) | `~/.local/share/opencode/` | `$XDG_DATA_HOME/opencode/` when set |
265
263
  | OpenCode **config** (AGENTS, skills, …) | `~/.config/opencode/` | still OpenCode-named for rule discovery |
@@ -331,7 +329,7 @@ const cursor = createCursor({
331
329
  // apiBaseURL: "https://api2.cursor.sh",
332
330
  // agentBaseURL: "https://agentn.us.api5.cursor.sh", // explicit Run host override
333
331
  // telemetryEnabled: true, // opt in to GetServerConfig telemetry
334
- // cacheDir: "/path/to/host/cache", // optional; else host heuristic / ~/.cache/opencode
332
+ // cacheDir: "/path/to/host/cache", // optional; else active-host heuristic
335
333
  // retry: { maxAttempts: 3, baseDelayMs: 500, maxDelayMs: 8_000 },
336
334
  // continuation: { heartbeatMs: 5_000, semanticIdleMs: 120_000, hardCapMs: 600_000 },
337
335
  })
@@ -340,7 +338,7 @@ const model = cursor.languageModel("composer-2.5")
340
338
  // model implements AI SDK LanguageModelV3 (doStream / doGenerate)
341
339
  ```
342
340
 
343
- Pass either `accessToken` (JWT from OAuth or key exchange) or `apiKey` (raw `crsr_...` key). 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 OCP `@opencode-compat/profile` detect (if installed) or the local MiMo/Kilo/OpenCode heuristic 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. 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`.
341
+ Pass either `accessToken` (JWT from OAuth or key exchange) or `apiKey` (raw `crsr_...` key). 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. 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`.
344
342
 
345
343
  ## Environment variables
346
344
 
@@ -352,7 +350,9 @@ Pass either `accessToken` (JWT from OAuth or key exchange) or `apiKey` (raw `crs
352
350
  | `CURSOR_PROVIDER_DEBUG` | Set to `1` or `true` to enable wire-level debug logging |
353
351
  | `CURSOR_PROVIDER_DEBUG_FILE` | Debug log path (default: `debug-<pid>.log` under `$TMPDIR/cursor-provider-logs-<uid>/`) |
354
352
  | `CURSOR_OPENCODE2_DEV_ENTRY` | **Local OpenCode 2.0 only.** Absolute path to a built entry file (usually `dist/index.js`). Rewrites the catalog's 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. |
355
- | `XDG_CACHE_HOME` | Base for host cache dirs (`$XDG_CACHE_HOME/opencode/`, `…/mimocode/`, or `…/kilo/`) when no explicit `cacheDir` / OCP detect override |
353
+ | `XDG_CACHE_HOME` | Base for host cache dirs (`$XDG_CACHE_HOME/opencode/`, `…/mimocode/`, `…/kilo/`, or OMP's migrated `…/omp/`) when no explicit `cacheDir` / OCP detect override |
354
+ | `PI_CODING_AGENT_DIR` | Custom Pi/OMP agent root; provider stores its private cache below `<agent-root>/cache/cursor-opencode/` |
355
+ | `PI_CONFIG_DIR` | Custom Pi/OMP config root name used when deriving the default agent root |
356
356
  | `MIMOCODE_HOME` | When set, host cache is `$MIMOCODE_HOME/cache` (MiMo) |
357
357
  | `KILO_CONFIG_DIR` | When set, host cache is `$XDG_CACHE_HOME/kilo` |
358
358
  | `XDG_DATA_HOME` | When set, OpenCode `auth.json` is read from `$XDG_DATA_HOME/opencode/` instead of `~/.local/share/opencode/` |
@@ -397,6 +397,7 @@ OpenCode
397
397
  | `src/debug.ts` | Opt-in wire-level debug logging (`CURSOR_PROVIDER_DEBUG`) |
398
398
  | `src/auth.ts` | PKCE OAuth, API key exchange, JWT refresh |
399
399
  | `src/models.ts` | `AvailableModels` fetch and `cursor-models.json` cache |
400
+ | `src/protocol/conversation-persistence.ts` | Atomic per-session restart snapshots under the 24-hour `cursor-conversations/` cache |
400
401
  | `src/context/paths.ts` | Host cache root + Cursor project metadata under `<host-cache>/projects/<slug>/` |
401
402
  | `src/agent-url.ts` | `GetServerConfig` fetch + in-process memo (region-specific Run host) |
402
403
  | `src/transport/connect.ts` | HTTP/2 bidi stream and unary RPC calls |
@@ -410,9 +411,11 @@ If this guidance causes issues, update `buildOpenCodeInteractionGuidance` in [`s
410
411
 
411
412
  ### Cursor edit handshakes
412
413
 
413
- 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. The provider retains that correlation and exposes an 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.
414
+ 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.
414
415
 
415
- The prerequisite native read also preserves an existing final LF/CRLF. OpenCode's numbered read envelope cannot represent that terminator, and dropping it made Cursor's exact edit matcher reject valid replacements, retry reads, and eventually rewrite the file. This restoration applies only to confirmed-complete, unbounded reads; paged and capped reads keep their existing truncation behavior.
416
+ 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.
417
+
418
+ 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.
416
419
 
417
420
  ### `apply_patch` models (GPT-5 and friends)
418
421
 
@@ -443,6 +446,7 @@ Kilo `scout` is reserved for external documentation, dependency repositories, an
443
446
  | `cursor-opencode-provider/plugin/v2` | OpenCode 1.18 Effect/Promise v2 plugin (`ctx.aisdk.*`) |
444
447
  | `cursor-opencode-provider/plugin/opencode2` | OpenCode 2.0 beta plugin (self-registering: catalog + auth + tools) |
445
448
  | `cursor-opencode-provider/errors` | Structured provider error classes |
449
+ | `cursor-opencode-provider/image-save` | Host-neutral `executeCursorImageSave` (pi-bridge / non-plugin hosts) |
446
450
 
447
451
  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.
448
452
 
@@ -459,9 +463,10 @@ The package root intentionally stays plugin-safe for OpenCode's classic loader.
459
463
  | 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. |
460
464
  | 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. |
461
465
  | `Unsupported Cursor exec variant …` | The error names the canonical Cursor CLI request field, its expected result field, and this provider's handling classification. `handling=unsupported` is a known Cursor-native capability without a safe OpenCode AI SDK bridge; `unknown request field` indicates new protocol drift; `handling=opencode-tool` or `provider-control` indicates a provider decoder/dispatch regression. Enable the debug log and report the full named error. |
462
- | 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. |
463
- | OpenCode compacts immediately after a tool-heavy task | Upgrade to a version containing the cumulative-usage fix. Cursor's final `TurnEnded` counters cover the entire held multi-step Run; older provider versions exposed that aggregate as the final request's context size, which made OpenCode compact far too early. Fixed versions keep the raw counters in `providerMetadata.cursor` and report a request-local context estimate instead. An over-budget guard also catches single-step or parallel Runs whose counters are cumulative even without a visible tool boundary. |
464
- | 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. Single-step Runs expose those counters directly. For multi-step Runs they remain in `providerMetadata.cursor` because Cursor reports only a cumulative whole-Run total, which cannot safely be used as one OpenCode request's context usage. With debug logging, confirm `turn_ended raw wire fields: … f4:wt0=0`. |
466
+ | 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. |
467
+ | OpenCode token totals grow once per tool call | Upgrade to a version containing the held-Run usage fix. OpenCode sums every AI SDK step, so the provider emits zero usage at intermediate tool boundaries and emits Cursor's final checkpoint total exactly once when the held Run closes. If the current Run has no token snapshot, the prior checkpoint total remains visible as stale; with no known snapshot, standard usage stays zero instead of mislabeling `TurnEnded` as context. Raw request counters remain in `providerMetadata.cursor`. |
468
+ | Context percentage jumps after a long tool turn or compaction | Cursor reports its authoritative context snapshot in checkpoint `token_details`. The provider makes the final AI SDK input + output equal `usedTokens`, proportionally partitions input into cached/uncached subsets, and exposes `maxTokens`, percentage, and category breakdown under `providerMetadata.cursor.context`. Intermediate held-Run tool boundaries remain zero, and compaction naturally replaces the prior snapshot with Cursor's smaller total. |
469
+ | 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`. |
465
470
 
466
471
  ## Security
467
472
 
@@ -470,19 +475,24 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
470
475
  ## Known limitations
471
476
 
472
477
  - **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.
473
- - **`TurnEnded` usage is whole-Run usage** — on a held multi-step Cursor Run, the final `interaction_update.turn_ended` counters aggregate every model invocation across tool boundaries. OpenCode usage is request-local and uses the latest assistant message to decide compaction, so publishing the aggregate there both double-counts earlier steps and causes premature compaction. The provider therefore uses a current-context estimate for multi-step AI SDK usage and preserves the exact cumulative counters in `providerMetadata.cursor`; single-step Runs map the counters directly only when they are close to the request-local estimate, while an over-budget guard handles cumulative no-boundary Runs. Cursor also consistently sends `cache_write=0` in captured Runs, so no alternate write count is available.
474
- - **`request_context` from OpenCode** — each Run sends Cursor `RequestContext` built from OpenCode project context (workspace env, `AGENTS.md` / `instructions`, `.opencode` agents/skills/plugins, git, layout, plus `.claude`/`.agents` skill fallbacks). Its canonical root is also used by the [injected system guidance](#injected-system-guidance). Same discovery as OpenCode — including `.cursor/` paths only when listed in `instructions`. 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.
478
+ - **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`; output/reasoning remain based on `TurnEnded`, while uncached/cache-read/cache-write input are proportionally normalized to the remaining context input. OpenCode can therefore display and compact against Cursor's total without double counting cache. 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. Intermediate tool-call boundaries report zero so a held Run is recorded only once. Cursor consistently sends `cache_write=0` in captured Runs, so no alternate write count is available.
479
+ - **`request_context` from OpenCode** — each Run sends Cursor `RequestContext` built from OpenCode project context (workspace env, `AGENTS.md` / `instructions`, `.opencode` agents/skills/plugins, git, layout, plus `.claude`/`.agents` skill fallbacks). Expensive workspace state (rules, environment, repository, git, and layout) is frozen per Cursor conversation and persisted for restart recovery. Tools/MCP descriptors, skills, subagents, and plugin metadata are rediscovered and overlaid each Run; unchanged overlays reuse byte-identical encoded context, while enabling/disabling a capability becomes visible on the next turn. 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). Same discovery as OpenCode — including `.cursor/` paths only when listed in `instructions`. 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.
475
480
  - **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`; exec still reconstructs the full OpenCode tool id.
476
481
  - **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` / `create_plan_tool_call`) into advertised OpenCode `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 and future unknown exec variants fail explicitly rather than receiving a guessed response that could deadlock the Run.
477
482
  - **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.
478
483
  - **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. The plugin registers its search fallback directly as `custom_websearch`, avoiding OpenCode's reserved `websearch` id filter for third-party providers and taking precedence over MCP search providers such as Brave without host environment configuration. 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.
479
484
  - **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.
480
485
  - **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`.
481
- - **Cursor-native interaction queries remain headless** — Cursor UI/approval *queries* (as distinct from display tool calls) still cannot be surfaced through the AI SDK provider interface. The normal system prompt redirects questions, planning, plan-mode transitions, and available web capabilities to executable host tools (`question`, `todowrite`, `plan_enter` / `plan_exit`, `custom_websearch`, `custom_webfetch`); native web/PR/MCP/image/SCM requests are declined so they remain behind host tool permissions. Separately from display `create_plan_tool_call` → `todowrite` mirroring, interaction `create_plan_request_query` is auto-acked (CLI headless parity) with success and an empty `plan_uri`, so Cursor may treat the plan as accepted without an OpenCode UI confirm. Compaction prompts are unchanged. Unknown future interaction variants fail the turn explicitly instead of hanging the Run stream.
482
- - **Compaction resets Cursor conversation state** — 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. Ordinary no-tool / `toolChoice:none` calls do not reset conversation state.
483
- - **Conversation bindings and compaction catalogs are bounded** — process-global per-session bindings, prior tool catalogs, and pending post-compaction rebases use a 256-session LRU bound. Evicting a conversation binding also drops its checkpoint and KV blobs.
486
+ - **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.
487
+ - **Cursor-native SwitchMode reaches you through OpenCode plan enter/exit** — when Cursor raises `switch_mode_request_query`, the provider translates it into the host `plan_enter` or `plan_exit` tool (when advertised) and keeps Cursor waiting until you approve or reject, matching Cursor CLI. `plan`/`spec` enter plan mode; every other non-empty target (including `agent`, `build`, `chat`, `debug`, `edit`, `background`, `multitask`, `triage`, `project`, and unknowns) leaves plan via `plan_exit`. After approval, the provider injects a Cursor CLI-shaped `<system_reminder>` for that mode on later turns so the agent gets the same behavioral instructions the CLI would send.
488
+ - **Cursor image generation saves through OpenCode's permission** — Cursor generates the image on its servers and then writes it to you like any other file. Because that write carries raw bytes, which OpenCode's text `write` tool cannot represent, the provider holds the bytes and asks OpenCode for the same permissions a normal write uses — the external-directory prompt when the target sits outside your project, then `edit` — naming the target file. Approve it and the image is written byte-for-byte to its path; deny it and nothing reaches disk — Cursor is told the write was denied, using its own permission-denied result, so the model knows the file is not there. Generation is only approved when the save path is available, so Cursor quota is never spent on an image that could not be saved. By default images land in the Cursor project folder under `assets/`, kept out of your git tree as agent artifacts. Ask for a specific path and the image is saved there instead. Editing an existing image with `reference_image_paths` is not supported yet. The approach is adapted from [`jkalasas/opencode-antigravity-image`](https://github.com/jkalasas/opencode-antigravity-image) (MIT) by jkalasas, which established writing image bytes from inside an OpenCode plugin tool.
489
+ - **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.
490
+ - **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.
491
+ - **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.
492
+ - **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, 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 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 compaction catalogs 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.
493
+ - **Oversized checkpoint graphs rebase instead of flooding the Run stream** — before reusing a checkpoint, the provider measures its reachable KV graph. State above 100 MiB, or an incomplete graph with a missing/undecodable reference, rotates to a fresh Cursor conversation seeded from OpenCode's authoritative history. 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.
484
494
  - **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.
485
- - **Large reads are capped at 50 KB by OpenCode, and the cap is reported** — OpenCode's `read` tool stops at `MAX_BYTES = 50 * 1024`, cutting on a whole-line boundary, and appends `(Output capped at 50 KB. Showing lines X-Y. Use offset=N to continue.)`. The provider strips that envelope before returning content to Cursor so the model cannot echo wrappers into a later write, but it re-states the cap: a capped native read appends a `[Partial read: …]` marker after the content, a capped MCP read gets a separate notice content item, and a Pi read carries structured `PiReadExecSuccess.truncation`. 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. Without that signal the model treats a capped read as the whole file and rewrites it truncated, discarding everything past the cap. 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 the only lever is paging with `offset`. Prefer `edit` over whole-file `write` on files above 50 KB.
495
+ - **Large reads are capped at 50 KB by OpenCode, and the cap is reported** — OpenCode's `read` tool stops at `MAX_BYTES = 50 * 1024`, cutting on a whole-line boundary, and appends `(Output capped at 50 KB. Showing lines X-Y. Use offset=N to continue.)`. The provider strips that envelope before returning content to Cursor so the model cannot echo wrappers into a later write, but it re-states the cap: a capped native read appends a `[Partial read: …]` marker after the content, a capped MCP read gets a separate notice content item, and a Pi read carries structured `PiReadExecSuccess.truncation`. 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. 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`.
486
496
  - **No fallback models** — if Cursor’s `AvailableModels` API is unreachable and there is no local cache, the provider exposes no models.
487
497
 
488
498
  ## License
@@ -5,7 +5,7 @@ export type CollectedAgent = {
5
5
  prompt: string;
6
6
  };
7
7
  /**
8
- * Custom agents from the active OpenCode-compatible config roots. OCP may
8
+ * Custom agents from the active OpenCode-compatible config roots. A host bridge may
9
9
  * install a host-neutral bridge before loading this module; without one this
10
10
  * remains the direct OpenCode discovery path.
11
11
  */
@@ -120,7 +120,7 @@ async function scanAgentRoot(root, out) {
120
120
  await scanDir(path.join(root, "agents"), "agents");
121
121
  }
122
122
  /**
123
- * Custom agents from the active OpenCode-compatible config roots. OCP may
123
+ * Custom agents from the active OpenCode-compatible config roots. A host bridge may
124
124
  * install a host-neutral bridge before loading this module; without one this
125
125
  * remains the direct OpenCode discovery path.
126
126
  */
@@ -1,6 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { opencodeGlobalDataDir } from "./paths.js";
3
+ import { hostGlobalDataDir } from "./paths.js";
4
4
  function debugEnabled() {
5
5
  return process.env.CURSOR_PROVIDER_DEBUG === "1" || process.env.CURSOR_PROVIDER_DEBUG === "true";
6
6
  }
@@ -26,7 +26,7 @@ export async function readStoredAuth(providerId) {
26
26
  return undefined;
27
27
  }
28
28
  }
29
- const filePath = path.join(opencodeGlobalDataDir(), "auth.json");
29
+ const filePath = path.join(hostGlobalDataDir(), "auth.json");
30
30
  try {
31
31
  const raw = await readFile(filePath, "utf-8");
32
32
  try {
@@ -4,9 +4,19 @@ export type BuildRequestContextInput = {
4
4
  tools?: OpencodeToolDef[];
5
5
  providerIdentifier?: string;
6
6
  };
7
+ export declare const DYNAMIC_REQUEST_CONTEXT_KEYS: readonly ["tools", "agent_skills", "custom_subagents", "mcp_file_system_options", "mcp_meta_tool_options", "web_search_enabled", "web_fetch_enabled", "agent_skills_info_complete", "custom_subagents_info_complete", "mcp_file_system_info_complete", "mcp_info_complete", "hooks_additional_context"];
8
+ export type DynamicRequestContextKey = typeof DYNAMIC_REQUEST_CONTEXT_KEYS[number];
7
9
  /**
8
10
  * Full RequestContext payload for live UMA + exec #10 reply.
9
11
  * Sourced from OpenCode discovery (and .claude/.agents skill fallbacks).
10
- * Honors `instructions` globs the same way OpenCode does (including `.cursor/` if listed).
12
+ * Honors `instructions` globs the same way OpenCode does. The provider never
13
+ * looks in Cursor's own directories; a `.cursor/` path is read only when the
14
+ * user's own OpenCode config lists it, exactly as the host would read it.
11
15
  */
12
16
  export declare function buildRequestContext(input: BuildRequestContextInput): Promise<Record<string, unknown>>;
17
+ /** Rediscover only capability/plugin sections that may change during a chat. */
18
+ export declare function buildDynamicRequestContext(input: BuildRequestContextInput): Promise<Record<string, unknown>>;
19
+ /** Keep expensive workspace state frozen while replacing every live capability field. */
20
+ export declare function materializeRequestContext(base: Record<string, unknown>, dynamic: Record<string, unknown>): Record<string, unknown>;
21
+ /** Strip live capability fields before retaining/persisting a conversation base. */
22
+ export declare function requestContextBase(context: Record<string, unknown>): Record<string, unknown>;
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { extractHostSubagentCatalog, toolsToDescriptors, toolsToMcpDescriptors, } from "../protocol/tools.js";
3
- import { collectRules } from "./rules.js";
3
+ import { collectRules, findGitWorktree, loadMergedConfig, } from "./rules.js";
4
4
  import { collectSkills } from "./skills.js";
5
5
  import { collectAgents } from "./agents.js";
6
6
  import { collectPlugins } from "./plugins.js";
@@ -9,6 +9,20 @@ import { collectProjectLayout } from "./layout.js";
9
9
  import { buildEnv } from "./env.js";
10
10
  import { ensureOpencodeProjectDir } from "./paths.js";
11
11
  import { traceRequestContextPaths } from "../debug.js";
12
+ export const DYNAMIC_REQUEST_CONTEXT_KEYS = [
13
+ "tools",
14
+ "agent_skills",
15
+ "custom_subagents",
16
+ "mcp_file_system_options",
17
+ "mcp_meta_tool_options",
18
+ "web_search_enabled",
19
+ "web_fetch_enabled",
20
+ "agent_skills_info_complete",
21
+ "custom_subagents_info_complete",
22
+ "mcp_file_system_info_complete",
23
+ "mcp_info_complete",
24
+ "hooks_additional_context",
25
+ ];
12
26
  const DEFAULT_HOST_SUBAGENTS = [
13
27
  {
14
28
  name: "general",
@@ -22,7 +36,7 @@ const DEFAULT_HOST_SUBAGENTS = [
22
36
  /**
23
37
  * Convert the raw host executor catalog into the exact subagent list advertised
24
38
  * to Cursor. `hostSubagents.complete` answers a narrow question: did the host's
25
- * task/actor tool itself expose an exhaustive recipient list (schema enum or
39
+ * task tool itself exposes an exhaustive recipient list (schema enum or
26
40
  * catalog marker)? OpenCode's task tool intentionally uses a plain string, so
27
41
  * that raw flag is false even though we can still advertise a complete Cursor
28
42
  * catalog by adding built-in defaults and discovered agent files.
@@ -46,19 +60,44 @@ function buildAdvertisedSubagentCatalog(hostSubagents, discoveredAgents) {
46
60
  /**
47
61
  * Full RequestContext payload for live UMA + exec #10 reply.
48
62
  * Sourced from OpenCode discovery (and .claude/.agents skill fallbacks).
49
- * Honors `instructions` globs the same way OpenCode does (including `.cursor/` if listed).
63
+ * Honors `instructions` globs the same way OpenCode does. The provider never
64
+ * looks in Cursor's own directories; a `.cursor/` path is read only when the
65
+ * user's own OpenCode config lists it, exactly as the host would read it.
50
66
  */
51
67
  export async function buildRequestContext(input) {
52
68
  const workspaceRoot = path.resolve(input.workspaceRoot || process.cwd());
69
+ const { rules, config, worktree } = await collectRules(workspaceRoot);
70
+ const [dynamic, git, layout] = await Promise.all([
71
+ buildDynamicRequestContextFromDiscovery(input, workspaceRoot, worktree, config),
72
+ collectGit(workspaceRoot),
73
+ collectProjectLayout(workspaceRoot),
74
+ ]);
75
+ const base = {
76
+ env: buildEnv(workspaceRoot),
77
+ rules: rules.map((r) => ({
78
+ full_path: r.fullPath,
79
+ content: r.content,
80
+ })),
81
+ repository_info: git.repositoryInfo,
82
+ git_repos: git.gitRepos,
83
+ project_layouts: [layout],
84
+ rules_info_complete: true,
85
+ env_info_complete: true,
86
+ repository_info_complete: true,
87
+ git_repo_info_complete: true,
88
+ git_status_info_complete: true,
89
+ };
90
+ const ctx = materializeRequestContext(base, dynamic);
91
+ traceRequestContextPaths("buildRequestContext", ctx);
92
+ return ctx;
93
+ }
94
+ async function buildDynamicRequestContextFromDiscovery(input, workspaceRoot, worktree, config) {
53
95
  const providerIdentifier = input.providerIdentifier ?? "opencode";
54
96
  const tools = input.tools ?? [];
55
- const { rules, config, worktree } = await collectRules(workspaceRoot);
56
- const [skills, agents, plugins, git, layout] = await Promise.all([
97
+ const [skills, agents, plugins] = await Promise.all([
57
98
  collectSkills(workspaceRoot, worktree),
58
99
  collectAgents(workspaceRoot),
59
100
  collectPlugins(workspaceRoot, config),
60
- collectGit(workspaceRoot),
61
- collectProjectLayout(workspaceRoot),
62
101
  ]);
63
102
  const mcpServerNames = Object.keys(config.mcp ?? {});
64
103
  const flat = toolsToDescriptors(tools, providerIdentifier, mcpServerNames);
@@ -84,16 +123,8 @@ export async function buildRequestContext(input) {
84
123
  `Delegate to the host-configured ${agent.name} subagent; its host instructions and tools apply.`,
85
124
  };
86
125
  });
87
- const ctx = {
88
- env: buildEnv(workspaceRoot),
126
+ const dynamic = {
89
127
  tools: flat,
90
- rules: rules.map((r) => ({
91
- full_path: r.fullPath,
92
- content: r.content,
93
- })),
94
- repository_info: git.repositoryInfo,
95
- git_repos: git.gitRepos,
96
- project_layouts: [layout],
97
128
  agent_skills: skills.map((s) => ({
98
129
  full_path: s.fullPath,
99
130
  content: s.content,
@@ -116,22 +147,42 @@ export async function buildRequestContext(input) {
116
147
  // custom_web* aliases instead of routing through a query doomed to fail.
117
148
  web_search_enabled: false,
118
149
  web_fetch_enabled: false,
119
- // Completeness: true only for sections we actually gathered.
120
- rules_info_complete: true,
121
- env_info_complete: true,
122
- repository_info_complete: true,
123
- git_repo_info_complete: true,
124
- git_status_info_complete: true,
125
150
  agent_skills_info_complete: true,
126
151
  custom_subagents_info_complete: advertisedSubagents.complete,
127
152
  mcp_file_system_info_complete: true,
128
153
  mcp_info_complete: true,
129
154
  };
130
155
  if (plugins.length > 0) {
131
- ctx.hooks_additional_context = plugins
156
+ dynamic.hooks_additional_context = plugins
132
157
  .map((p) => `opencode-plugin:${p.source}:${p.id}`)
133
158
  .join("\n");
134
159
  }
135
- traceRequestContextPaths("buildRequestContext", ctx);
136
- return ctx;
160
+ return dynamic;
161
+ }
162
+ /** Rediscover only capability/plugin sections that may change during a chat. */
163
+ export async function buildDynamicRequestContext(input) {
164
+ const workspaceRoot = path.resolve(input.workspaceRoot || process.cwd());
165
+ const [worktree, config] = await Promise.all([
166
+ findGitWorktree(workspaceRoot),
167
+ loadMergedConfig(workspaceRoot),
168
+ ]);
169
+ return buildDynamicRequestContextFromDiscovery(input, workspaceRoot, worktree, config);
170
+ }
171
+ /** Keep expensive workspace state frozen while replacing every live capability field. */
172
+ export function materializeRequestContext(base, dynamic) {
173
+ const context = structuredClone(base);
174
+ for (const key of DYNAMIC_REQUEST_CONTEXT_KEYS)
175
+ delete context[key];
176
+ for (const key of DYNAMIC_REQUEST_CONTEXT_KEYS) {
177
+ if (Object.hasOwn(dynamic, key))
178
+ context[key] = structuredClone(dynamic[key]);
179
+ }
180
+ return context;
181
+ }
182
+ /** Strip live capability fields before retaining/persisting a conversation base. */
183
+ export function requestContextBase(context) {
184
+ const base = structuredClone(context);
185
+ for (const key of DYNAMIC_REQUEST_CONTEXT_KEYS)
186
+ delete base[key];
187
+ return base;
137
188
  }
@@ -0,0 +1,29 @@
1
+ import { type BuildRequestContextInput } from "./build.js";
2
+ export declare const MAX_FROZEN_REQUEST_CONTEXTS = 256;
3
+ /** Frozen stable RequestContext base for this conversation, if any. */
4
+ export declare function getFrozenRequestContext(conversationId: string): Record<string, unknown> | undefined;
5
+ /** Replace the frozen stable base, stripping any live capability fields. */
6
+ export declare function setFrozenRequestContext(conversationId: string, context: Record<string, unknown>): void;
7
+ /** Drop a conversation's frozen RequestContext (compaction / binding reset). */
8
+ export declare function clearFrozenRequestContext(conversationId: string): void;
9
+ /**
10
+ * Move a stable workspace base across a conversation-id reset.
11
+ *
12
+ * Compaction changes Cursor's state/checkpoint identity, not the OpenCode
13
+ * workspace. Preserve the expensive base and the prior materialized bytes as a
14
+ * comparison seed; getOrBuildRequestContext still rediscovers live capability
15
+ * overlays and only reuses the complete context when those bytes also match.
16
+ */
17
+ export declare function transferFrozenRequestContext(previousConversationId: string, nextConversationId: string): boolean;
18
+ /** Test helper — wipe all frozen contexts. */
19
+ export declare function resetFrozenRequestContextsForTests(): void;
20
+ /**
21
+ * Return a stable-base + live-overlay RequestContext for `conversationId`.
22
+ * The base is built once; capability sections are rediscovered every Run.
23
+ */
24
+ export declare function getOrBuildRequestContext(conversationId: string, input: BuildRequestContextInput, opts?: {
25
+ refresh?: boolean;
26
+ }): Promise<{
27
+ context: Record<string, unknown>;
28
+ reused: boolean;
29
+ }>;