Skip to content

feat(cli): cache Cursor per-model config options from capability probes - #344

Open
Tigatron wants to merge 3 commits into
LodyAI:mainfrom
Tigatron:feat/cursor-acp-model-catalog
Open

feat(cli): cache Cursor per-model config options from capability probes#344
Tigatron wants to merge 3 commits into
LodyAI:mainfrom
Tigatron:feat/cursor-acp-model-catalog

Conversation

@Tigatron

@Tigatron Tigatron commented Sep 3, 2026

Copy link
Copy Markdown

Related issue

Refs #343

Problem / pressure

Lody's registry Cursor agent runs in cursor-agent's legacy "variants" mode because the ACP client never declares clientCapabilities._meta.parameterizedModelPicker: model ids arrive exploded, no per-model options are published, and an in-session model switch through session/set_config_option silently fails to match. Once the picker is enabled the second problem surfaces: a session/new snapshot describes only the model current at probe time, while Cursor rebuilds thinking/effort/context/fast per model and rejects values the new model lacks, so nothing outside a live session can know another model's options. The run-config applier also sent per-model options before the model when the model lived in configOptionValues, which Cursor validates against the previous model.

Summary

  • Registry Cursor (identity gate: cliType: 'registry' and agentType: 'cursor') declares parameterizedModelPicker at initialize; AgentClient gains a generic requestExtMethod with abort support.
  • The explicit machine/acp-capabilities-refresh probe calls Cursor's side-effect-free cursor/list_available_models once after session/new and stores every model's non-model/mode options as AcpCapabilityCacheEntry.configOptionsByModel ([] = a known model with no options; missing key = unknown model). JSON-RPC -32601 means no catalog; a response that fails Zod validation or lists a model twice, a timeout, an abort, or any other failure fails the probe with [ACP_CAPABILITIES_INCOMPLETE] so the Settings Test action can retry. Real sessions never fetch it; their snapshot write preserves the stored catalog for the same sourceVersion, and the unchanged-entry comparison includes the field.
  • @lody/shared gains the one composition rule resolveAcpConfigOptionsForModel (snapshot options no catalog entry owns + the selected model's entry; model/mode always from the snapshot; unknown model → snapshot), resolveAcpTargetModelId, and true/false toggle predicates; Fast mode now recognises a select whose values are exactly true/false.
  • The run-config applier switches the model before applying per-model options and no longer resends the model inside the option loop; the existing unstable_setSessionModel channel and its fallback are unchanged.
  • No RPC schema, SessionDoc, or ACP_CAPABILITY_CACHE_VERSION change; consumers of the catalog (composer, MCP mapping) follow in separate PRs.

Before / after

Before After
Cursor initialize without parameterizedModelPicker: exploded model ids, no per-model options, silent model-switch failures. Clean model ids plus per-model options; session/set_config_option model switches take effect.
Capability cache holds one probe-time option snapshot. Cache additionally holds configOptionsByModel from an explicit probe, preserved across same-version session writes.
Per-model options could be applied before the model switch. The model is applied first; options are validated by the agent against the selected model.

Test plan

  • cd packages/shared && corepack pnpm exec tsgo --noEmit && corepack pnpm exec vitest run tests/acp-run-config.test.ts — 29 passed (composition rule, target-model resolution, toggle predicates).
  • cd apps/cli && corepack pnpm exec tsgo --noEmit && corepack pnpm exec vitest run src/agent/cursor-acp.test.ts src/agent/acp-capabilities.test.ts src/agent/agent-client-initialize.test.ts src/lib/loro/machine-document-capabilities.test.ts src/session/acp-session-config-applier.test.ts tests/message-handler-fable-fast-mode.test.ts tests/session-execution-service.test.ts — all passed (catalog parsing, -32601, INCOMPLETE paths with fake timers, identity-gated initialize meta, same-version catalog preservation and change detection, applier ordering).
  • Root corepack pnpm typecheck, corepack pnpm lint (0 errors), corepack pnpm format:check, corepack pnpm lint:i18n, check:code-collab-imports, check:platform-boundaries, check:public-boundary — passed.
  • corepack pnpm test:ci — all packages pass except two pre-existing apps/cli worktree tests (session-manager.test.ts "rebuilds a prepared worktree whose directory disappeared before adoption", worktree-manager.create.test.ts "should rebuild a missing registered worktree") that fail identically on a clean main with git 2.51.0; unrelated to this change.
  • Real cursor-agent 2026.08.31 protocol probe (no prompt sent): cursor/list_available_models returned 37 unique models whose option shapes match the parser (all select, thinking/fast values false/true, context category model_config, some models with no options); an unknown ext method returned -32601. The captured payload parses through fetchCursorModelCatalog into 37 entries.
  • Desktop app end to end with an isolated LODY_DATA_DIR: add registry Cursor, refresh → Ready; a turn with Thinking on and effort high completes without a rejected-selection notice; the same session switched to gpt-5.4 runs a second turn cleanly. Skipped: Windows/Linux runs; a real -32601 agent (covered by unit tests only).

Context handoff

Instructions for reviewing agents

  • Review focus: apps/cli/src/agent/cursor-acp.ts (validation and error mapping of the catalog response), MachineDocument.updateAcpCapabilities in apps/cli/src/lib/loro/doc.ts (same-sourceVersion preservation and the change detector including the new field), and resolveAcpConfigOptionsForModel in packages/shared/src/acp-run-config.ts (union rule and the model/mode exclusion).
  • Decisions to challenge: gating the opt-in on registry identity rather than the launched binary; falling back to the snapshot for a model the catalog does not list instead of failing the probe; keeping ACP_CAPABILITY_CACHE_VERSION unchanged because the field is additive.
  • Plausible failures / evidence gaps: cursor-agent could change the ext method or option shapes (only 2026.08.31 was observed); the agent-client.tscursor-acp.ts import cycle is function-scoped only; the applier still logs (not warns) when a config-option model switch is rejected, unchanged from before.

Authoring context

  • User goal / directives: Let Lody expose Cursor's per-model thinking, effort, context, and fast options by fetching the catalog when the agent is added or its Test/refresh action runs, and let existing Cursor sessions benefit without per-session migration; keep the change surface small after an earlier, broader attempt was ablated.
  • Constraints / non-goals: No RPC schema, SessionDoc, or cache-version change; no catalog fetch from inside sessions; no auto-probe on agent creation and no empty-catalog guard (deferred by decision); composer UI and MCP mapping land in follow-up PRs; [Bug] Grok 4.6 reasoning effort picker is missing Medium (uses probed model's ladder) #149/fix(acp): pick the reasoning-effort ladder from the selected model #286 (modelReasoningEfforts for Grok) is sibling work this composes with.
  • Risk-bearing decisions: Enabling parameterizedModelPicker changes Cursor's advertised model ids for new probes and sessions; the catalog is stored only from explicit probes and preserved across same-version session writes; enumeration through session/set_config_option is deliberately forbidden because it rewrites the user's global Cursor config.
  • Destructive or irreversible behavior: None; the probe writes one Machine Flock row per agent config, and a failed probe leaves the previous entry untouched.
  • Deliberately not done or tested: Composer rendering and MCP semantic mapping (separate PRs); catalog completeness against the snapshot model list; Windows/Linux runs.
  • Unknowns / confidence: High confidence in the parser against the observed 2026.08.31 payload and in the storage contract; the ext method is a Cursor-side undocumented extension and could change without notice.

Tigatron and others added 3 commits September 3, 2026 16:42
An ACP probe snapshot only describes the model that was current when the
probe ran, while an agent such as cursor-agent publishes a distinct option
set for every model. AcpCapabilityCacheEntry gains configOptionsByModel,
the per-model catalog an explicit probe may store, where a model mapped to
an empty list has no model-dependent options and a missing key means the
catalog does not know that model.

resolveAcpConfigOptionsForModel is the one composition rule for a model's
options: the snapshot options no catalog entry owns plus the selected
model's entry, falling back to the snapshot for an unknown model. model and
mode options always come from the snapshot, so a catalog can neither shrink
the model picker nor replace the permission modes. resolveAcpTargetModelId
names the model a run config targets.

Fast mode now also recognises a select whose values are exactly true and
false, the shape cursor-agent uses for boolean parameters, and writes back
the advertised representation instead of on/off.

Implemented with cursor-grok-4.6-xhigh-fast subagents.

Model: claude-fable-5.1
Co-authored-by: Cursor <cursoragent@cursor.com>
Registry Cursor now declares clientCapabilities._meta.parameterizedModelPicker
at initialize, so probes and sessions see clean model ids plus per-model
thinking, effort, context, and fast options instead of exploded variant
strings whose in-session model switch silently fails. The gate is registry
identity, never a same-named custom or builtin config.

A session/new snapshot only describes the model current at probe time, so an
explicit machine/acp-capabilities-refresh additionally calls the agent's
cursor/list_available_models once and stores every model's options as
AcpCapabilityCacheEntry.configOptionsByModel. That method is the only
extension cursor-agent serves and performs no writes, unlike enumerating
models through session/set_config_option, which rewrites the user's global
Cursor config. Real sessions never fetch the catalog; their snapshot write
keeps the stored catalog for the same sourceVersion, and the unchanged-entry
comparison includes it so a refreshed catalog is committed. JSON-RPC -32601
means no catalog; a response that fails validation or lists a model twice,
a timeout, or any other failure fails the probe with
[ACP_CAPABILITIES_INCOMPLETE] so the settings test button can retry.

Implemented with cursor-grok-4.6-xhigh-fast subagents.

Model: claude-fable-5.1
Co-authored-by: Cursor <cursoragent@cursor.com>
Registry and custom agents carry the selected model in the model config
option rather than modelId, and the applier switched it inside the option
loop at its key position. Cursor validates thinking, effort, and fast against
the model that is current when each option arrives, so options ordered
before the model key were checked against the previous model and rejected.
Apply the config-option model right after the explicit modelId path and skip
its loop entry; the unstable_setSessionModel channel and its fallback are
unchanged.

Implemented with cursor-grok-4.6-xhigh-fast subagents.

Model: claude-fable-5.1
Co-authored-by: Cursor <cursoragent@cursor.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-03T10:06:46.444324Z 2675f4f PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant