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.

@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-05T10:32:44.103640Z 3f62760 New commits
ℹ️ 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.

Tigatron and others added 3 commits September 5, 2026 18:15
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>
@Tigatron
Tigatron force-pushed the feat/cursor-acp-model-catalog branch from 2675f4f to 3f62760 Compare September 5, 2026 10:26
@Tigatron

Tigatron commented Sep 5, 2026

Copy link
Copy Markdown
Author

Rebased onto main (b0e0b97) to clear the merge conflict; head is now 3f62760.

The only textual conflict was in apps/cli/src/lib/loro/doc.ts against #395: main changed updateAcpCapabilities to return Promise<AcpCapabilityCacheEntry> and added modelReasoningEfforts to serializeAcpCapabilityWithoutFetchTime, while this PR widens options with configOptionsByModel and adds that field to the same serializer. Both sides are kept (the returned entry now also carries configOptionsByModel). packages/shared/src/ai.ts, session-execution-service.ts, and machine-document-capabilities.test.ts auto-merged with orthogonal hunks. git range-diff shows commits 1 and 3 unchanged; commit 2 differs only by that resolution.

Re-verified on the rebased head: workspace typecheck, lint (0 errors), format:check, lint:i18n, check:code-collab-imports, check:platform-boundaries, check:public-boundary; packages/shared tests/acp-run-config.test.ts (29 passed) and the seven apps/cli test files from the test plan (130 passed).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f627604d4

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1792 to +1793
? { _meta: { parameterizedModelPicker: true } }
: {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Invalidate pre-parameterized Cursor capability caches

When upgrading an installation with an existing registry Cursor capability row, this opt-in changes Cursor from exploded variant IDs to clean parameterized model IDs, but ACP_CAPABILITY_CACHE_VERSION remains 6 and the source version remains cursor@2026.08.31. The existing row therefore stays authoritative, so selectors and dispatch can send a cached legacy model ID that the newly initialized agent no longer advertises, causing the first post-upgrade turn to reject the requested model or run with the default until the user manually refreshes capabilities. Invalidate or migrate those prior Cursor cache entries when enabling this capability.

AGENTS.md reference: apps/cli/src/agent/AGENTS.md:L243-L249

Useful? React with 👍 / 👎.

Comment thread packages/shared/src/ai.ts
* with no model-dependent options maps to `[]`, while a missing key means the
* catalog does not know that model. Absent when the agent exposes no catalog.
*/
configOptionsByModel?: Record<string, AcpConfigOptionSummary[]>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Accept the catalog in the refresh response schema

For a remote-machine Cursor refresh that returns this new field, executeAcpRefresh includes the complete capability in MachineAcpCapabilitiesRefreshResponse, but the nested AcpCapabilityCacheEntrySchema in packages/shared/src/message-schemas.ts is strict and does not declare configOptionsByModel. Consequently parseRpcSuccessResult rejects the otherwise successful response and returns null, which the desktop reports as a capability-refresh timeout; the local IPC path bypasses that parser, explaining why local end-to-end testing does not expose the failure. Add the field to the transport schema or omit it from the RPC response.

Useful? React with 👍 / 👎.

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