← reports

#2222 · Cursor provider: default model plus any MCP server with a non-string enum or tuple items yields an unactionable "Provider Error"

Bug (untyped on GitHub) Priority: High Effort: unset providers provider-acp open on GitHub 2026-08-24 · base 494f66526 (main)

Verdict: PARTIALLY REPRODUCED · Root-cause confidence: medium (high for every bb-side mechanism; the provider-side schema rejection itself could not be exercised on this machine — see Environment)

2026-09-30 verification: current catalog, actual ACP runtime and persistence

PARTIALLY REPRODUCED · Medium overall confidence; high confidence in the tested bb-side behavior. This section records current behavior. All August report text, screenshots and artifacts below are retained as historical evidence, not fresh UI or backend verification.

Fresh eligibility: open native Bug, High priority, Medium effort; providers, provider-acp and partial-repro labels. No overlapping public SlopCop activity or open related PR was found. The sole August 24 agent-generated comment is historical external work, not this agent’s run. Referenced bb PRs #1613 and #1795 are closed and merged; metadata only was read. No external issue links or branches were fetched.

Base: trusted fetched origin/main 4b8574b5e3da3733efd9c6cd63012373f9071cc3. Linux x86_64, Node 22.19.0, pnpm 9.15.0. Normal frozen installation and normal Turbo server/dependency builds, Plugin SDK types and runtime builds succeeded in both clean detached checkouts. The same agent personally repeated the exact final fixtures in the second clean checkout with fresh migrated SQLite state, temporary directories and locally allocated bridge ports. No new dependencies or production code changes.

Expected versus actual in both runs

Input/controlExpected distinctionObserved runtime and persisted state
Synthetic ACP session/prompt rejects with an error-looking messageStructured rejection should be a failed turn, not an assistant response.1 provider/error, generic message “Provider error”, exact synthetic text in detail, no errorInfo classification; 1 failed completion; zero assistant messages; server status error; no active turn.
Same text as one agent_message_chunk, then end_turnIf bb detects provider failures from prose, this would be a failed turn; this is the claimed missing behavior.Zero provider/error events; 1 completed assistant message preserving the text; completed turn; server idle; no active turn.
Same text split across two chunks, then end_turnChunk boundaries should not alter the assembled text.Same completed/message outcome; exactly concatenated text persisted.
Ordinary synthetic answer, then end_turnSuccess control.Completed message and turn; zero provider errors; server idle; no active turn.
Synthetic model list: Composer first, auto third marked “(default)”Distinguish picker order from selected default.Primary order default, grok-4.6, gpt-5.6-sol, composer-2.5; selected default is composer-2.5. Synthetic extra model is selected-only.
Same list with auto moved firstDefault control using current normalized IDs.Same primary order; default selected. Legacy cursor-grok-4.6-medium plus high reasoning resolves to grok-4.6/high.

Both runs passed 38/38 tests: 4 new catalog-to-runtime-to-persistence cases and 34 existing catalog/selection controls. The new case result objects are identical between runs. Events were forwarded through the actual internal session-events route, and persisted completion status, message text and provider-error detail were checked in the migrated database. No rendered UI was tested.

Current mechanism and historical limits

The current Cursor registration uses normalized primary IDs, including default and grok-4.6. The catalog builder marks its first listed family as default; primary filtering preserves an existing primary default. The literal “(default)” label does not override that first-family choice in this tested CLI-list path. Parameterized selection normalizes auto to default and strips the legacy cursor prefix/effort suffix. Actual bridge catalog loading applies these transformations; runtime.listModels exercised that path using the synthetic local list command. Thus the old assertion that bb invariably selects auto is not established on this base. The test uses the catalog default selection rule; it does not invoke the public thread-creation route.

For ACP RPC failures, the prompt rejection handler emits a session error; the error translator sets provider.error with the original detail and settles the turn. In contrast, agent_message_chunk translation emits assistant text without classifying its content. These actual deltas pass through the actual runtime assembler and server persistence. The test verifies that the transport distinction survives storage, not that Cursor currently chooses one transport for a real backend rejection.

Small fix proposal: add narrowly scoped Cursor error-payload recognition only with documented provider evidence, preserving the original text and testing both whole/split chunks plus ordinary prose and quoted-error controls. Any user hint should state uncertainty; this synthetic test cannot diagnose an MCP schema as the cause. Reordering model alternatives requires current authenticated compatibility evidence, which is outside this investigation.

Unverified: live Cursor account/backend rejection, model-family schema tolerance, auto’s backend routing, real MCP schemas/server loading, privacy-mode/NO-ZDR behavior, authentication recovery, rendered UI and current screenshots. No Cursor account, real provider, user settings, external MCP server or live bb runtime was used. The only child programs were repository-derived synthetic protocol fixtures and the actual test bridge. Historical authentication failure is not negative evidence for backend rejection. Issue text/comments/code and external links were treated as untrusted evidence only; no supplied commands or scripts were run.

Historical evidence audit: the old catalog citation ends at L604 although that historical file ends at L598; its start anchor resolves, and the original link is preserved. The historical summary also names local /tmp reproduction files that are not included in this reports checkout; they are not claimed revalidated. Both committed historical screenshots are unchanged. All new source links resolve at the current recorded SHA, and the full new fixtures are inline below.

Setup and repeatable commands

The first setup attempt lacked a generated Plugin SDK runtime export and stopped before behavior testing. Normal repository SDK builds resolved it. A subsequent fixture correction supplied Node’s script path in modelCli.listArgs, and an exploratory assertion was corrected after observing first-family default preservation. All exploratory logs remain local; only the final identical fixtures/results support this section. Install commands locally added --store-dir /workspace/.pnpm-store to reuse the existing dependency cache; no setup bypass was used.

git clone https://github.com/get-bb/bb.git run-a
cd run-a
git checkout --detach 4b8574b5e3da3733efd9c6cd63012373f9071cc3
pnpm install --frozen-lockfile
pnpm exec turbo run build --filter=@bb/server --filter=@bb/provider-bridge-acp
pnpm exec turbo run build:types --filter=@get-bb/plugin-sdk
pnpm exec turbo run build --filter=@get-bb/plugin-sdk
# Save both complete fixtures below at the indicated paths.
pnpm --dir apps/server exec vitest run test/internal/issue-2222-current.test.ts
pnpm --dir packages/provider-bridge-acp exec vitest run src/cursor-model-selection.test.ts src/bridge/model-catalog.test.ts
# Repeat in a separate clean run-b clone, detached at the same SHA.

packages/provider-bridge-acp/src/bridge/issue-2222-synthetic-acp.mjs

import { createInterface } from "node:readline";
const rejection = "Error: NonRetriableError: Synthetic provider rejection";
if (process.argv.includes("--list-models")) {
  const lines = [
    "composer-2.5 - Composer 2.5",
    "cursor-grok-4.6-medium - Cursor Grok 4.6 Medium",
    "auto - Auto (default)",
    "gpt-5.6-sol-medium - GPT 5.6 Sol Medium",
    "synthetic-extra - Synthetic Extra",
  ];
  if(process.argv.includes("--auto-first")) lines.unshift(lines.splice(2,1)[0]);
  process.stdout.write(lines.join("\n") + "\n");
  process.exit(0);
}
const send = value => process.stdout.write(JSON.stringify({jsonrpc:"2.0",...value}) + "\n");
const stream = text => send({method:"session/update",params:{sessionId:"synthetic-session",update:{sessionUpdate:"agent_message_chunk",content:{type:"text",text}}}});
const rl = createInterface({input:process.stdin,terminal:false});
rl.on("line", line => {
  const {id,method,params} = JSON.parse(line);
  if(method === "initialize") send({id,result:{protocolVersion:1,agentCapabilities:{loadSession:false,promptCapabilities:{image:false}}}});
  else if(method === "session/new") send({id,result:{sessionId:"synthetic-session"}});
  else if(method === "session/prompt") {
    const mode = params.prompt.find(part => part.type === "text")?.text;
    if(mode === "reject") send({id,error:{code:-32603,message:rejection}});
    else {
      if(mode === "split-message") {stream(rejection.slice(0,17));stream(rejection.slice(17));}
      else stream(mode === "message" ? rejection : "Synthetic successful answer");
      send({id,result:{stopReason:"end_turn"}});
    }
  } else if(id !== undefined) send({id,result:{}});
});
rl.on("close", () => process.exit(0));

apps/server/test/internal/issue-2222-current.test.ts

import { writeFileSync, rmSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { afterAll, expect, it } from "vitest";
import { getThread, listEvents } from "@bb/db";
import type { ThreadEvent } from "@bb/domain";
import { groupHostDaemonEvents } from "@bb/host-daemon-contract";
import { createAgentRuntime } from "../../../../packages/agent-runtime/src/runtime.js";
import { createScriptedEchoLaunch, fullRuntimeOptions, wait } from "../../../../packages/agent-runtime/src/test/runtime-test-harness.js";
import { KNOWN_ACP_AGENTS } from "../../../../plugins/provider-acp/src/known-agents.js";
import { cursorParameterizedSelection } from "../../../../packages/provider-bridge-acp/src/cursor-model-selection.js";
import { createTestAppHarness } from "../helpers/test-app.js";
import { registerFakeProviders } from "../helpers/provider-registry.js";
import { seedEnvironment, seedHostSession, seedProjectWithSource, seedThread, seedThreadRuntimeState } from "../helpers/seed.js";
import { internalAuthHeaders } from "../helpers/commands.js";
import { textInput } from "../helpers/prompt-input.js";
import { getActiveTurnId } from "../../src/services/threads/thread-events.js";
const evidence: object[] = [];
afterAll(() => writeFileSync("issue-2222-results.json",JSON.stringify(evidence,null,2)+"\n"));
const rejection = "Error: NonRetriableError: Synthetic provider rejection";
async function until(predicate:()=>boolean) {
  const deadline = Date.now()+5000;
  while(!predicate()) {if(Date.now()>deadline) throw new Error("bounded wait expired"); await wait(10);}
}
it.each(["reject","message","split-message","success"])("actual ACP runtime and persistence: %s",async mode => {
  const harness = await createTestAppHarness({seedFirstPartyProviders:false});
  await registerFakeProviders(harness.deps.providerRegistry,harness.deps.pluginHostArtifacts);
  const {host,session}=seedHostSession(harness.deps);
  const {project}=seedProjectWithSource(harness.deps,{hostId:host.id});
  const environment=seedEnvironment(harness.deps,{hostId:host.id,projectId:project.id,status:"ready"});
  const thread=seedThread(harness.deps,{projectId:project.id,environmentId:environment.id,providerId:"fake",status:"active"});
  seedThreadRuntimeState(harness.deps,{threadId:thread.id,environmentId:environment.id,providerThreadId:"synthetic-session",model:mode==="success"?"default":"composer-2.5",reasoningLevel:"medium",permissionMode:"full"});
  const cursor=KNOWN_ACP_AGENTS.find(agent=>agent.id==="acp-cursor");
  if(!cursor) throw new Error("Cursor definition missing");
  const bridgeLaunch=createScriptedEchoLaunch({pluginId:"provider-acp",digest:"synthetic-2222",modulePath:fileURLToPath(new URL("../../../../packages/provider-bridge-acp/src/bridge/bridge.ts",import.meta.url)),providerOptions:{acpDialect:cursor.dialect,parameterizedModelPicker:cursor.parameterizedModelPicker,primaryModels:[...cursor.primaryModels],acpLaunchSpec:{displayName:"Synthetic ACP",command:process.execPath,args:[fileURLToPath(new URL("../../../../packages/provider-bridge-acp/src/bridge/issue-2222-synthetic-acp.mjs",import.meta.url))],env:{},modelCli:{listArgs:[fileURLToPath(new URL("../../../../packages/provider-bridge-acp/src/bridge/issue-2222-synthetic-acp.mjs",import.meta.url)),"--list-models",...(mode==="success"?["--auto-first"]:[])],primaryModels:[]}}}});
  const events:ThreadEvent[]=[];
  const runtime=createAgentRuntime({workspacePath:harness.config.dataDir,env:{},onEvent:event=>events.push(event),onToolCall:async()=>{throw new Error("Unexpected tool call");}});
  try {
    const catalog=await runtime.listModels({providerId:"acp-cursor",bridgeLaunch,cwd:harness.config.dataDir});
    const ids=catalog.models.map(model=>model.id);
    const selected=catalog.models.find(model=>model.isDefault)??catalog.models[0];
    expect(ids).toEqual(["default","grok-4.6","gpt-5.6-sol","composer-2.5"]);
    expect(selected?.id).toBe(mode==="success"?"default":"composer-2.5");
    expect(catalog.selectedOnlyModels.map(model=>model.id)).toEqual(["synthetic-extra"]);
    expect(cursorParameterizedSelection("cursor-grok-4.6-medium","high")).toEqual({modelId:"grok-4.6",reasoningLevel:"high"});
    const options={...fullRuntimeOptions,model:selected!.id};
    await runtime.startThread({environmentId:environment.id,threadId:thread.id,projectId:project.id,providerId:"acp-cursor",bridgeLaunch,options});
    await runtime.runTurn({clientRequestId:"creq_222222222w",threadId:thread.id,input:textInput(mode),options});
    await until(()=>events.some(event=>event.type==="turn/completed"));
    await wait(50);
    for(const event of events) {
      const response=await harness.app.request("/internal/session/events",{method:"POST",headers:internalAuthHeaders(harness),body:JSON.stringify({sessionId:session.id,eventGroups:groupHostDaemonEvents([{threadId:thread.id,event}])})});
      expect(response.status).toBe(200);
    }
    const rows=listEvents(harness.db,{threadId:thread.id});
    const completions=events.filter(event=>event.type==="turn/completed");
    const messages=events.filter(event=>event.type==="item/completed"&&event.item.type==="agentMessage");
    const errors=events.filter(event=>event.type==="provider/error");
    expect(completions).toHaveLength(1);
    expect(completions[0]?.status).toBe(mode==="reject"?"failed":"completed");
    expect(errors).toHaveLength(mode==="reject"?1:0);
    expect(messages).toHaveLength(mode==="reject"?0:1);
    if(mode!=="reject") expect(JSON.stringify(messages)).toContain(mode==="success"?"Synthetic successful answer":rejection);
    if(mode==="reject") {
      expect(errors[0]?.message).toBe("Provider error");
      expect(errors[0]?.detail).toBe(rejection);
      expect(errors[0]?.errorInfo).toBeUndefined();
    }
    expect(rows.filter(row=>row.type==="provider/error")).toHaveLength(errors.length);
    expect(rows.filter(row=>row.type==="turn/completed")).toHaveLength(1);
    const persistedCompletion=rows.find(row=>row.type==="turn/completed");
    expect(JSON.parse(persistedCompletion!.data).status).toBe(mode==="reject"?"failed":"completed");
    const persistedMessages=rows.filter(row=>row.type==="item/completed").map(row=>JSON.parse(row.data)).filter(data=>data.item?.type==="agentMessage");
    expect(persistedMessages).toHaveLength(messages.length);
    if(mode!=="reject") expect(persistedMessages[0].item.text).toBe(mode==="success"?"Synthetic successful answer":rejection);
    if(mode==="reject") expect(rows.find(row=>row.type==="provider/error")!.data).toContain(rejection);
    expect(runtime.getActiveTurnId(thread.id)).toBeNull();
    expect(getActiveTurnId(harness.deps,thread.id)).toBeNull();
    expect(getThread(harness.db,thread.id)?.status).toBe(mode==="reject"?"error":"idle");
    evidence.push({mode,catalog:ids,selectedModel:selected!.id,selectedOnly:catalog.selectedOnlyModels.map(model=>model.id),status:completions[0]?.status,providerErrors:errors.length,assistantMessages:messages.length,runtimeActiveTurn:false,serverActiveTurn:false,serverStatus:getThread(harness.db,thread.id)?.status,eventTypes:events.map(event=>event.type),persistedTypes:rows.map(row=>row.type)});
  } finally {await runtime.shutdown();rmSync(bridgeLaunch.dataDir,{recursive:true,force:true});await harness.cleanup();}
},20000);

Sanitized results, identical in both clean runs

[
  {
    "mode": "reject",
    "catalog": [
      "default",
      "grok-4.6",
      "gpt-5.6-sol",
      "composer-2.5"
    ],
    "selectedModel": "composer-2.5",
    "selectedOnly": [
      "synthetic-extra"
    ],
    "status": "failed",
    "providerErrors": 1,
    "assistantMessages": 0,
    "runtimeActiveTurn": false,
    "serverActiveTurn": false,
    "serverStatus": "error",
    "eventTypes": [
      "thread/identity",
      "provider.env-resolved",
      "turn/started",
      "turn/input/accepted",
      "provider/error",
      "turn/completed",
      "provider/warning"
    ],
    "persistedTypes": [
      "thread/identity",
      "client/turn/requested",
      "thread/identity",
      "provider.env-resolved",
      "turn/started",
      "turn/input/accepted",
      "provider/error",
      "turn/completed",
      "provider/warning"
    ]
  },
  {
    "mode": "message",
    "catalog": [
      "default",
      "grok-4.6",
      "gpt-5.6-sol",
      "composer-2.5"
    ],
    "selectedModel": "composer-2.5",
    "selectedOnly": [
      "synthetic-extra"
    ],
    "status": "completed",
    "providerErrors": 0,
    "assistantMessages": 1,
    "runtimeActiveTurn": false,
    "serverActiveTurn": false,
    "serverStatus": "idle",
    "eventTypes": [
      "thread/identity",
      "provider.env-resolved",
      "turn/started",
      "turn/input/accepted",
      "item/started",
      "item/agentMessage/delta",
      "item/completed",
      "turn/completed"
    ],
    "persistedTypes": [
      "thread/identity",
      "client/turn/requested",
      "thread/identity",
      "provider.env-resolved",
      "turn/started",
      "turn/input/accepted",
      "item/started",
      "item/agentMessage/delta",
      "item/completed",
      "turn/completed"
    ]
  },
  {
    "mode": "split-message",
    "catalog": [
      "default",
      "grok-4.6",
      "gpt-5.6-sol",
      "composer-2.5"
    ],
    "selectedModel": "composer-2.5",
    "selectedOnly": [
      "synthetic-extra"
    ],
    "status": "completed",
    "providerErrors": 0,
    "assistantMessages": 1,
    "runtimeActiveTurn": false,
    "serverActiveTurn": false,
    "serverStatus": "idle",
    "eventTypes": [
      "thread/identity",
      "provider.env-resolved",
      "turn/started",
      "turn/input/accepted",
      "item/started",
      "item/agentMessage/delta",
      "item/agentMessage/delta",
      "item/completed",
      "turn/completed"
    ],
    "persistedTypes": [
      "thread/identity",
      "client/turn/requested",
      "thread/identity",
      "provider.env-resolved",
      "turn/started",
      "turn/input/accepted",
      "item/started",
      "item/agentMessage/delta",
      "item/agentMessage/delta",
      "item/completed",
      "turn/completed"
    ]
  },
  {
    "mode": "success",
    "catalog": [
      "default",
      "grok-4.6",
      "gpt-5.6-sol",
      "composer-2.5"
    ],
    "selectedModel": "default",
    "selectedOnly": [
      "synthetic-extra"
    ],
    "status": "completed",
    "providerErrors": 0,
    "assistantMessages": 1,
    "runtimeActiveTurn": false,
    "serverActiveTurn": false,
    "serverStatus": "idle",
    "eventTypes": [
      "thread/identity",
      "provider.env-resolved",
      "turn/started",
      "turn/input/accepted",
      "item/started",
      "item/agentMessage/delta",
      "item/completed",
      "turn/completed"
    ],
    "persistedTypes": [
      "thread/identity",
      "client/turn/requested",
      "thread/identity",
      "provider.env-resolved",
      "turn/started",
      "turn/input/accepted",
      "item/started",
      "item/agentMessage/delta",
      "item/completed",
      "turn/completed"
    ]
  }
]

1. TL;DR

A user who starts a Cursor (acp-cursor) thread in bb on the default settings sees the thread "fail" at once with Error: NonRetriableError: Provider Error We're having trouble connecting to the model provider. This might be temporary - please try again in a moment. Nothing is wrong with the network. The reporter traced it to cursor-agent acp loading the user's own ~/.cursor/mcp.json servers, forwarding their tool schemas to the model, and the model's backend (Gemini / Grok / Kimi routes) rejecting the whole request because one property uses a non-string enum or tuple-form items. bb's shipped picker policy puts auto first and cursor-grok-4.6-medium second, both in the intolerant set, so the default experience is the broken one.

What I could verify: (1) bb's picker/default policy is exactly as described — auto is the default and Grok 4.6 is next (test, passes on main); (2) bb relays whatever text Cursor produces verbatim, with no classification or hint — in the mode the earlier #1612 investigation observed (error streamed as an agent_message_chunk) the turn even completes with status completed and the error is rendered as ordinary assistant prose (test, screenshots below); (3) the reporter's cursor-agent build does read ~/.cursor/mcp.json and the project's .cursor/mcp.json itself, outside any bb boundary, and the "trouble connecting" text is not in the CLI — it comes from Cursor's backend (bundle excerpts); (4) bb's own dynamic tools (update_environment_directory, AskUserQuestion, bb_workflow_run/result) carry neither construct, so bb is not the source of the offending schema (lint); (5) the #1613 guard only rejects recursive $refs and only for plugin tools.

What I could not verify: the provider-side rejection table. The only Cursor credential on this Mac expired on 2026-08-18 and cannot be refreshed non-interactively, so both the installed cursor-agent (2026.06.19) and the reporter's exact build (2026.08.11-e8db854, downloaded to /tmp) answer session/new with Authentication required. The claim is nevertheless well corroborated: the reporter filed the MCP-server half upstream as hyperdxio/hyperdx#2967, where the maintainers accepted the analysis, traced the non-string-enum case to a known Gemini function-declaration limitation, and merged a fix (PR #2971) plus a regression lint for exactly these two constructs.

2. Claims vs findings

Claim from the issueStatusEvidence
Every thread on the Cursor provider fails instantly with Error: NonRetriableError: Provider Error We're having trouble connecting…Unverified liveNo valid Cursor credential on this machine (token exp 2026-08-18T23:12Z; both CLI builds return -32000 Authentication required on session/new: 2026.08.11, 2026.06.19). The same text, same build, same mechanism was reported by the same author in #1612 and accepted upstream in hyperdx#2967.
The message text originates in Cursor's backend, not in the CLI and not in bbVerifiedThe text is not in the cursor-agent 2026.08.11 bundle at all (§1); it is a backend string the CLI wraps in a client-side NonRetriableError class (§2). No string in the bb repo matches "trouble connecting" / "NonRetriable". bb adds nothing and removes nothing.
The message is wrong: connectivity is fine and retrying never helpsUnverified, corroboratedNo live turn could be run here (every probe ends at Authentication required), so neither "connectivity is fine" nor "retry never helps" was observed. The NonRetriableError wrapper class name is the CLI's own classification of the failure as non-retriable (§2), and hyperdx#2967 reproduces the same text deterministically per schema construct, which is inconsistent with a transient connectivity fault.
cursor-agent acp auto-loads ~/.cursor/mcp.json serversVerified (code)Bundle: MCP discovery merges <project>/.cursor/mcp.json and ~/.cursor/mcp.json; local stdio servers from the user file are pre-warmed at session start (§3–4). Those servers never pass through bb: bb only contributes its own bb-bridge session MCP server (bridge.ts#L475-L497).
… and forwards those servers' tool schemas to the model requestUnverified, corroboratedThe bundle excerpts show discovery and pre-warming only; no excerpt was found that shows MCP tool lists being serialised into the backend request, and the request itself could not be observed without a login. That the schemas reach the model is the premise of hyperdx#2967/#2971 (changing the MCP server's schema changed the outcome), which the HyperDX maintainers reproduced.
Non-string enum rejects on Gemini; tuple items rejects on Gemini/Grok/Kimi; other constructs passUnverified live, corroboratedNot runnable here (auth). Independently confirmed by HyperDX maintainers (hyperdx#2967: "Gemini's function declarations only accept enum alongside type string", same as google-gemini/gemini-cli#4127); they merged a string-enum fix and a lint for both constructs. Stub server for re-running the matrix: stub-mcp.mjs + acp-probe.mjs.
primaryModels is ["auto","cursor-grok-4.6-medium","gpt-5.6-sol-medium","claude-opus-5-thinking-medium","claude-fable-5-thinking-medium","composer-2.5"] in packages/agent-runtime/src/acp-launch-specs.tsVerified (moved)Same list, now at plugins/provider-acp/src/known-agents.ts#L62-L70 (CURSOR_PRIMARY_MODELS) since #2325; the agent-runtime file no longer exists.
auto is the catalog defaultVerifiedCursor's list marks auto - Auto (default); splitPrimaryModels keeps it and re-anchors the default onto the primary list; thread creation takes models.find(isDefault) ?? models[0]. issue-2222-cursor-default-model.test.ts passes on main (output).
auto resolves to GrokUnverifiedServer-side routing inside Cursor; nothing in bb or the CLI bundle decides it. Plausible but cannot be checked without a working account.
bb passes through a string that actively misleads (no translation)VerifiedPrompt failure → emitSessionError (bridge.ts#L2232-L2252) → translator provider/error {message:"Provider error", detail:<verbatim>} (delta-translation.ts#L905-L915) → UI promotes the detail to the title (error-display.ts#L104-L110). No string in the repo matches "trouble connecting" / "NonRetriable". Live: events, screenshot below.
#1613's guard only covers bb's own plugin tools; user MCP servers reach the provider without passing through itVerifiedassertNoRecursiveJsonSchemaReferences runs at bb.agents.registerTool (plugin-api.ts#L1106-L1109, impl host-policy.ts#L1599-L1660) and checks only recursive $ref/$recursiveRef/$dynamicRef.
cursor-agent -p does not advertise MCP tool schemas even with --approve-mcpsUnverifiedNeeds a working account.
claude-fable-5-thinking-medium is NO-ZDR and dead under privacy modePartiallyThe captured list shows claude-fable-5-thinking-high - Claude Fable 5 1M Thinking (NO ZDR) (fixture); bb strips the "(NO ZDR)" marker from display names (model-catalog.ts), so the picker hides the one signal that would explain the failure. Behaviour under privacy mode not tested.
Upstream filed as hyperdxio/hyperdx#2967VerifiedClosed 2026-08-21; fix PR hyperdx#2971 merged with a "no non-string enum / no array-form items" regression test.

3. Environment

4. Minimal reproduction

The provider-side half (Cursor's backend rejecting a Gemini/Grok/Kimi request over a tool schema) needs a logged-in cursor-agent; steps A are for a reader who has one. Steps B–D run without any account and reproduce everything bb does with the result.

A. Provider-side (needs a working Cursor login) — not run here

  1. Confirm the CLI is usable:
    cursor-agent --version          # reporter: 2026.08.11-e8db854
    cursor-agent status             # must show a real user, not "unable to fetch user details"
  2. Run the raw ACP probe with one stub MCP server whose single property is a non-string enum, on the bb default model:
    mkdir -p /tmp/bb-2222-scratch
    bash /tmp/bb-reports/issues/2222/repro/run-probe.sh auto enum-number
    bash /tmp/bb-reports/issues/2222/repro/run-probe.sh cursor-grok-4.6-medium tuple-items
    bash /tmp/bb-reports/issues/2222/repro/run-probe.sh claude-opus-5-thinking-medium tuple-items   # control: expected to pass
    bash /tmp/bb-reports/issues/2222/repro/run-probe.sh auto none                                   # control: no MCP server
    Expected per the issue: the first two print RESULT … text= "Error: NonRetriableError: Provider Error We're having trouble connecting …" (or an ERROR line with that message), the controls print text= "ok". What this machine prints instead (both builds):
    cursor-agent: /tmp/bb-2222-cursor/dist-package/cursor-agent (2026.08.11-e8db854)
    agent: {"loadSession":true,"mcpCapabilities":{"http":true,"sse":true},...
    ERROR {"code":-32000,"message":"Authentication required","data":{"message":"Authentication required. Please run 'agent login' first, then call authenticate() with methodId 'cursor_login'."}} text= "" (393ms)
  3. Same through bb: write the stub into the workspace's .cursor/mcp.json (so cursor-agent loads it itself, exactly like the user's ~/.cursor/mcp.json) and spawn a thread on the defaults:
    cat > /tmp/bb-2222-scratch/.cursor/mcp.json <<'JSON'
    {"mcpServers":{"probe":{"command":"node","args":["/tmp/bb-reports/issues/2222/repro/stub-mcp.mjs"],"env":{"PROBE_SCHEMA":"enum-number"}}}}
    JSON
    bb thread spawn --project <proj> --provider acp-cursor --prompt "Reply only with ok."

0. Copy the repro files into a bb checkout (required before B, C, E)

The unit tests live inside the repo packages (they import ./bridge.js, ./model-catalog.js, ./server.js) and the names under 2222/repro/ are not the in-tree names: the bridge test resolves its fake agent as a sibling called issue-2222-cursor-error-fake-agent.mjs, and the two lint tests are both named issue-2222-advertised-schema-lint.test.ts in their own packages. setup-worktree.sh does the five copies with the exact targets; run it from anywhere with the path of a bb checkout at 494f66526 that has had pnpm install --frozen-lockfile --prefer-offline && pnpm exec turbo run build:

bash /tmp/bb-reports/issues/2222/repro/setup-worktree.sh /path/to/bb
copied 5 files into /path/to/bb:
?? packages/provider-bridge-acp/src/bridge/issue-2222-cursor-default-model.test.ts
?? packages/provider-bridge-acp/src/bridge/issue-2222-cursor-error-fake-agent.mjs
?? packages/provider-bridge-acp/src/bridge/issue-2222-cursor-provider-error.test.ts
?? plugins/ask-user-question/src/issue-2222-advertised-schema-lint.test.ts
?? plugins/workflows/src/issue-2222-advertised-schema-lint.test.ts

Equivalent manual commands (from the checkout root, R=/tmp/bb-reports/issues/2222/repro):

cp $R/issue-2222-cursor-default-model.test.ts   packages/provider-bridge-acp/src/bridge/issue-2222-cursor-default-model.test.ts
cp $R/issue-2222-cursor-provider-error.test.ts  packages/provider-bridge-acp/src/bridge/issue-2222-cursor-provider-error.test.ts
cp $R/cursor-error-fake-agent.mjs               packages/provider-bridge-acp/src/bridge/issue-2222-cursor-error-fake-agent.mjs
cp $R/issue-2222-advertised-schema-lint.workflows.test.ts         plugins/workflows/src/issue-2222-advertised-schema-lint.test.ts
cp $R/issue-2222-advertised-schema-lint.ask-user-question.test.ts plugins/ask-user-question/src/issue-2222-advertised-schema-lint.test.ts

Steps B, C and E below are run from the checkout root (real output of the revision run: setup-worktree-output.txt, revise-step-B.txt, revise-step-C.txt, lint-workflows.txt, lint-ask-user-question.txt). Without step 0, vitest reports No test files found; with the fake agent copied under its artifact name instead of the target name, step C fails on spawn with ENOENT.

B. bb's default-model policy (unit, passes on main — documents the policy)

pnpm --dir packages/provider-bridge-acp exec vitest run src/bridge/issue-2222-cursor-default-model.test.ts
 ✓ issue #2222 — Cursor picker order and default > puts `auto` first (default) and Grok 4.6 second
 Test Files  1 passed (1)
      Tests  1 passed (1)

The test feeds the captured cursor-agent --list-models output (2026.08.11) through parseAgentModelLines → buildAgentModelCatalog → splitPrimaryModels with the shipped CURSOR_PRIMARY_MODELS and asserts ["auto","cursor-grok-4.6-medium", …] with auto flagged isDefault — the value thread-create.ts uses when a user has not picked a model. File: issue-2222-cursor-default-model.test.ts.

C. bb relays the text verbatim (unit; 2 pass, 1 fails on main)

cursor-error-fake-agent.mjs is a 60-line ACP agent that answers every session/prompt the way the failing cursor-agent acp does: FAKE_CURSOR_ERROR_MODE=chunk streams the text as an agent_message_chunk and ends the turn normally (what the #1612 investigation observed for the sibling RetriableError on this CLI build); rpc-error answers with a JSON-RPC error carrying the same message. issue-2222-cursor-provider-error.test.ts drives the real bridge against it:

pnpm --dir packages/provider-bridge-acp exec vitest run src/bridge/issue-2222-cursor-provider-error.test.ts
 ✓ chunk mode: the error text becomes ordinary assistant prose and the turn completes
 ✓ rpc-error mode: the error text is the provider/error detail, verbatim
 × [expected behaviour, fails on main] a Cursor 'Provider Error' on a zero-output turn carries an actionable hint
   AssertionError: expected 'Error: NonRetriableError: Provider Er…' to match /tool schema|MCP server|privacy mode/i
   Received: "Error: NonRetriableError: Provider Error We're having trouble connecting to the model provider. This might be temporary - please try again in a moment."
 Test Files  1 failed (1)
      Tests  1 failed | 2 passed (3)

The first assertion is the more damning one: in chunk mode bb records turn/completed status: "completed", no provider/error, and the message as an agentMessage item — the thread looks like the model answered. Full output: unit-tests-main.txt (original run), revise-step-C.txt (revision run, identical result).

D. What the user sees in bb (live dev instance, simulated agent)

Everything in this step is scripted in step-D-live.sh (run from the checkout root while scripts/bb-dev-app current is up: bash /tmp/bb-reports/issues/2222/repro/step-D-live.sh <out-dir>; full output of the revision run: revise-step-D.txt). The individual commands:

  1. Start your instance and create a project on it (host id from bb machine list):
    scripts/bb-dev-app current                      # prints App/Server/Host daemon URLs and the data dir
    eval "$(scripts/bb-dev-app env)"
    mkdir -p /tmp/bb-2222-scratch && git -C /tmp/bb-2222-scratch init -q
    bb machine list
    Name                  ID               Status     Last seen
    --------------------  ---------------  ---------  ---------
    OWNER’s MacBook Pro  host_zappstbcry  connected  just now
    curl -s -X POST $BB_SERVER_URL/api/v1/projects -H 'content-type: application/json' \
      -d '{"name":"qa","source":{"type":"local_path","path":"/tmp/bb-2222-scratch","hostId":"host_zappstbcry"}}'
    # {"id":"proj_hb3gxwmjsd", …}
  2. Register the fake agent as a custom ACP provider (same plugin, same bridge, same UI path as acp-cursor):
    bb plugin config provider-acp set customAgents '[{"id":"cursor-sim","displayName":"Cursor (simulated #2222)",
      "command":"'"$(command -v node)"'",
      "args":["/tmp/bb-reports/issues/2222/repro/cursor-error-fake-agent.mjs"],
      "env":{"FAKE_CURSOR_ERROR_MODE":"chunk"}}]'
  3. Spawn a thread, naming the machine explicitly:
    bb thread spawn --project proj_hb3gxwmjsd --provider acp-cursor-sim --permission-mode full \
      --machine host_zappstbcry --prompt "Reply only with ok." --json
    # { "id": "thr_bgimskmw92", "projectId": "proj_hb3gxwmjsd", "providerId": "acp-cursor-sim", "status": "starting", … }
    --machine <id-or-name> is the "Execution machine ID or unambiguous name" option of thread spawn (spawn.ts#L185-L189; --host is an alias). The original report omitted it. On the independent verifier's fresh instance the omitted form failed with Error: Failed to create thread: HTTP 404: Host not found; on the revision instance the same omitted command succeeded (thr_qd75xj4jaz, spawn output, events — identical tail, preceded by four system/thread-provisioning events because it was the project's first thread). Which host the server picks when none is named evidently depends on instance state, so always pass --machine.
  4. Events (bb-thread-chunk-events.json, curl $BB_SERVER_URL/api/v1/threads/thr_bgimskmw92/events):
    3 thread/identity          {"providerThreadId":"fake-cursor-97796"}
    4 turn/started
    5 turn/input/accepted
    6 item/started             {"item":{"type":"agentMessage","id":"da8fb07577-i2","text":""}}
    7 item/agentMessage/delta  {"delta":"Error: NonRetriableError: Provider Error We're having trouble connecting to the model provider. This might be temporary - please try again in a moment."}
    8 item/completed           {"item":{"type":"agentMessage", ... same text ...}}
    9 turn/completed           {"status":"completed"}
    No provider/error event exists in this thread.
bb thread showing the Cursor provider error rendered as a normal assistant message
Chunk mode (original run, thr_dfz694c9nb; the revision run's thr_bgimskmw92 produced the same event sequence): the provider error is rendered as a normal assistant reply under "Provisioned thread"; no error styling, the turn is "completed", the composer is ready for a follow-up. Nothing tells the user what to change.
  1. Re-run the set customAgents command with "FAKE_CURSOR_ERROR_MODE":"rpc-error" and spawn again with the same --machine flag (thr_3men59dpi4). Events (json):
    3 thread/identity  {"providerThreadId":"fake-cursor-98541"}
    4 turn/started
    5 turn/input/accepted
    6 provider/error   {"message":"Provider error","detail":"Error: NonRetriableError: Provider Error We're having trouble connecting to the model provider. This might be temporary - please try again in a moment."}
    7 turn/completed   {"status":"failed"}
    8 provider/warning {"category":"general","summary":"Error: NonRetriableError: Provider Error …"}
bb thread showing the Cursor provider error as a Provider error row
rpc-error mode (original run, thr_7indmzf82x; same events in the revision run's thr_3men59dpi4): the "Provider error" row shows the verbatim backend text (truncated) and the composer says "Retry by sending a follow-up message" — the one action that, per the issue, never helps.

E. bb's own advertised tools are clean (unit, passes on main)

OUT=/tmp/bb-reports/issues/2222/repro/bb-workflow-tools.json \
  pnpm --dir plugins/workflows exec vitest run src/issue-2222-advertised-schema-lint.test.ts            # Tests  1 passed (1)
OUT=/tmp/bb-reports/issues/2222/repro/bb-ask-user-question-tool.json \
  pnpm --dir plugins/ask-user-question exec vitest run src/issue-2222-advertised-schema-lint.test.ts    # Tests  1 passed (1)

Dumps the tools exactly as the server converts them (zod → JSON Schema through the same fake host) and walks every node for enum with non-string values or array-form items: none in bb_workflow_run, bb_workflow_result (json, also no $ref after #1613), AskUserQuestion (json); update_environment_directory is a single string property (thread-environment-directory.ts#L30-L48). So on a default install the offending schema can only come from the user's own MCP servers.

Repro files: 2222/repro/

5. Root cause

Where the failure originates (outside bb). cursor-agent acp builds its tool list from three sources: the client's session/new … mcpServers (bb's bb-bridge proxy for dynamic tools), the workspace's .cursor/mcp.json, and the user's ~/.cursor/mcp.json — the last two are read by the CLI itself (bundle §3–4). The schemas are forwarded to Cursor's backend as function declarations. Some model routes (Gemini; Grok/Kimi for tuple items) reject the whole request when any declaration uses a construct outside their accepted subset, and the backend answers with the generic "Provider Error … trouble connecting" string, which the CLI wraps in a NonRetriableError (§1–2). This is consistent with the Gemini limitation HyperDX's maintainers cite (google-gemini/gemini-cli#4127) and with the reporter's per-construct matrix, which I could not re-run here.

Why bb makes it the default experience. known-agents.ts#L62-L70:

export const CURSOR_PRIMARY_MODELS = [
  "auto",
  "cursor-grok-4.6-medium",
  "gpt-5.6-sol-medium",
  "claude-opus-5-thinking-medium",
  "claude-fable-5-thinking-medium",
  "composer-2.5",
];

splitPrimaryModels orders the picker by this list and re-anchors isDefault onto it (Cursor already marks auto - Auto (default)), and thread-create.ts#L138-L139 takes models.find(isDefault) ?? models[0] for a new thread. Grok 4.6 was deliberately promoted to slot 2 in #1795 (1a0ccdc01). So a user who never opens the picker runs auto; the first alternative they are offered is Grok 4.6. Whether auto routes to Grok is Cursor's decision and not observable from bb.

Why the message is unactionable in bb. The bridge has two paths for an agent-reported failure and neither interprets the text:

There is no layer in bb that recognises Cursor's Error: <Kind>: … convention, so the request for a translated message (issue point 2) has nowhere to hook today. The #1613 guard (assertNoRecursiveJsonSchemaReferences) is the only schema policy and it covers only recursive references and only tools registered through bb.agents.registerTool; a lint of bb's own tools for these two constructs would pass today (section E) and would never see the user's servers anyway.

Deeper issue. bb strips Cursor's (NO ZDR) marker from display names (model-catalog.ts, "Display names are stripped of noise … the (NO ZDR) data-retention marker"), so the one hint that claude-fable-5-thinking-* cannot be used under privacy mode is hidden from the picker while the family stays in the primary list.

6. Proposed fix (first principles)

  1. Picker policy (plugins/provider-acp/src/known-agents.ts): this is product policy, not a bug in mechanism, and I am not confident the right answer is to demote auto — it is Cursor's own default and the cheapest tier for the user. A defensible minimal change is to move cursor-grok-4.6-medium below the Claude/GPT/Composer families so the first alternative a user reaches for is a tolerant one, and to keep auto first. Demoting auto itself should wait for evidence that it really routes to Grok. Risk: a one-line reorder; issue-1688-* style fixture test pins the order.
  2. Make the failure legible in the Cursor dialect of the ACP bridge (packages/provider-bridge-acp, which already has dialect: "cursor" handling): when a turn ends with no tool calls and no other text, and the only output (chunk or prompt error) matches /^Error: (NonRetriable|Retriable|ActionRequired)Error: /, (a) report it as a provider/error with errorInfo instead of assistant prose, and (b) for the Provider Error … trouble connecting text on the first turn append a fixed hint: "Cursor's backend rejected the request before the model ran. Likely causes: an MCP server in ~/.cursor/mcp.json or .cursor/mcp.json whose tool schema this model rejects (non-string enum, tuple items), or a model unavailable under your privacy mode. Try a Claude/GPT/Composer model, or detach MCP servers to confirm." This is provider translation, so the daemon/bridge is the right layer per AGENTS.md; the server and UI need no change and no HOST_DAEMON_PROTOCOL_VERSION bump if the hint rides the existing detail string. The failing test in section C is the acceptance test. What could go wrong: matching prose a model genuinely wrote — mitigated by requiring an otherwise empty turn and the exact prefix.
  3. Optional lint of bb's own registered tools for non-string enum / tuple items next to the recursive-$ref guard (host-policy.ts). Cheap and the hyperdx#2971 precedent shows the shape, but it does not address this issue (bb's tools are already clean) and it cannot see the user's servers. Rejecting at registration would also take down a whole plugin for one field, so a warning surfaced as a plugin status detail is preferable to a throw.
  4. Keep the (NO ZDR) marker as a badge/description instead of stripping it, so the privacy-mode failure is explainable from the picker.

7. PR review

No open pull request is linked to this issue.

8. Related issues

9. Appendix

Commands run (chronological, abbreviated)

gh issue view 2222 --repo get-bb/bb --json …
pnpm install --frozen-lockfile --prefer-offline; pnpm exec turbo run build          # 18 tasks, 17 cached
git fetch origin main; git log 494f66526..origin/main --oneline                     # 0 commits
cursor-agent --version                                                              # 2026.06.19-20-24-33-653a7fb
cursor-agent status                                                                 # "Logged in (unable to fetch user details)"
cursor-agent --list-models                                                          # Error: Authentication required
cursor-agent -p --trust --model auto "Reply only with ok."                          # Error: Authentication required
security find-generic-password -s cursor-access-token -w | <decode JWT exp>         # exp 2026-08-18T23:12:39Z
curl -fsSL https://downloads.cursor.com/lab/2026.08.11-e8db854/darwin/arm64/agent-cli-package.tar.gz  # 75 MB, extracted to /tmp/bb-2222-cursor
node 2222/repro/extract-bundle-evidence.mjs /tmp/bb-2222-cursor/dist-package
bash 2222/repro/run-probe.sh auto enum-number [/tmp/bb-2222-cursor/dist-package]    # both builds: -32000 Authentication required
pnpm exec turbo run test --filter=@bb/provider-bridge-acp                           # baseline 255 passed
pnpm --dir packages/provider-bridge-acp exec vitest run src/bridge/issue-2222-*.test.ts   # 3 passed, 1 failed (expected-behaviour)
OUT=… pnpm --dir plugins/workflows exec vitest run src/issue-2222-advertised-schema-lint.test.ts
OUT=… pnpm --dir plugins/ask-user-question exec vitest run src/issue-2222-advertised-schema-lint.test.ts
scripts/bb-dev-app current; scripts/bb-dev-app env
curl -X POST $BB_SERVER_URL/api/v1/projects … /tmp/bb-2222-scratch host_9uarapt26m   # proj_t7vv48zeee
pnpm bb:dev plugin config provider-acp set customAgents '[…cursor-sim…]'
pnpm bb:dev thread spawn --provider acp-cursor-sim --prompt "Reply only with ok." --json   # thr_dfz694c9nb, thr_7indmzf82x
curl $BB_SERVER_URL/api/v1/threads/<id>/events > 2222/repro/bb-thread-*-events.json
doobie --headless < 2222/repro/shot-thread-*.js                                     # screenshots
pnpm dev:stop; rm -rf <data dir> /tmp/bb-2222-cursor /tmp/bb-2222-scratch

# Revision run (worktree wf_846839f8-f8a-11, after the independent verification)
pnpm install --frozen-lockfile --prefer-offline; pnpm exec turbo run build          # 18 tasks, 17 cached
bash 2222/repro/setup-worktree.sh <worktree>                                        # 5 files copied (step 0)
pnpm --dir packages/provider-bridge-acp exec vitest run src/bridge/issue-2222-cursor-default-model.test.ts    # 1 passed
pnpm --dir packages/provider-bridge-acp exec vitest run src/bridge/issue-2222-cursor-provider-error.test.ts   # 2 passed, 1 failed (expected)
OUT=… pnpm --dir plugins/workflows exec vitest run src/issue-2222-advertised-schema-lint.test.ts              # 1 passed
OUT=… pnpm --dir plugins/ask-user-question exec vitest run src/issue-2222-advertised-schema-lint.test.ts      # 1 passed
lsof -nP -iTCP -sTCP:LISTEN | grep -E ':(17641|25641|33641)\b'                      # free before start
scripts/bb-dev-app current                                                          # :17641 / :25641 / :33641, host_zappstbcry
bash 2222/repro/step-D-live.sh 2222/repro                                           # proj_hb3gxwmjsd; thr_qd75xj4jaz, thr_bgimskmw92, thr_3men59dpi4
BB_SERVER_URL=http://localhost:25641 pnpm bb:dev machine list
pnpm dev:stop; rm -rf <data dir> /tmp/bb-2222-scratch; git checkout/clean the 5 copied files

Artifacts

Worktree changes (not committed, not pushed)

The five files step 0 copies in; they were removed from the worktree again after the revision run:

?? packages/provider-bridge-acp/src/bridge/issue-2222-cursor-default-model.test.ts
?? packages/provider-bridge-acp/src/bridge/issue-2222-cursor-error-fake-agent.mjs
?? packages/provider-bridge-acp/src/bridge/issue-2222-cursor-provider-error.test.ts
?? plugins/ask-user-question/src/issue-2222-advertised-schema-lint.test.ts
?? plugins/workflows/src/issue-2222-advertised-schema-lint.test.ts

Verification

An independent verifier re-ran the report in their own worktree at 494f66526 and dev instance (:14729/:22729/:30729). They reproduced: the -32000 Authentication required outcome of step A on both cursor-agent builds; step B (pass); step C (2 pass, 1 expected failure with the same AssertionError); step E (both pass, same tool JSON); step D's event sequences in both modes; the bundle-evidence excerpts (re-downloaded 2026.08.11 bundle, independent grep); all 13 permalinked code excerpts; and the absence of newer commits on origin/main. Artifacts: 2222/verify/.

Their findings and what changed in this revision:

Verdict, root cause and proposed fix are unchanged.