#4152 · Grouped ACP selections fail parsing

Bug · Priority: Medium · Effort: Low · providers, provider-acp

GitHub issue · 2026-09-23 · Base 94d77da09568d7f0f7cdb23b84686e743acfb0ad

REPRODUCED · Root-cause confidence: high

1. TL;DR

The ACP session-result parser rejects model choices wrapped in groups. It validates each top-level entry as a leaf choice and requires a string value there. A group contains nested choices instead, so parsing fails before model selection. The same parser silently discards grouped choices for other settings. Both results were repeated in a second clean checkout; no vendor account or real provider process was used.

2. Claims vs findings

ClaimFindingEvidence
Grouped model choices prevent session creationVerified at the session-result boundaryThe production schema throws in both runs; the connection rejects schema failures.
Flat options workVerifiedThe flat control passes twice.
Non-model groups disappearVerifiedThe regression assertion receives [] instead of two choices.
Specific vendor and desktop behaviorUnverifiedNo vendor binary, credentials, or UI was used.
The external ACP specification permits groupingNot independently checkedThe rule forbids fetching issue-supplied external links. This report demonstrates parser behavior directly.

3. Environment

Public get-bb/bb origin/main at the commit above; Darwin arm64; Node v22.22.3; pnpm 9.15.0 through Corepack. Frozen installs succeeded in both checkouts. The full Turbo build completed: 60 successful tasks. The default pnpm launcher was broken; a temporary Corepack shim resolved it. No dependency was added. No server, port, runtime store, or provider session was started.

4. Minimal reproduction

  1. Check out the recorded trusted commit in a fresh get-bb/bb checkout.
  2. Install and build:
    corepack pnpm install --frozen-lockfile --prefer-offline
    corepack pnpm exec turbo run build
  3. Save the authored test as packages/provider-bridge-acp/src/grouped-options.repro.test.ts.
  4. Run:
    pnpm exec turbo run test --filter=@bb/provider-bridge-acp -- --run src/grouped-options.repro.test.ts

Expected: all three tests pass, preserving both choices. Actual: the flat control passes; grouped model parsing throws and non-model choices become empty.

Tests  2 failed | 1 passed (3)
Invalid ACP model config option: options[0].value expected string, received undefined
AssertionError: expected [] to deeply equal [ { value: 'local-fast', …(1) }, …(1) ]

The nested model diagnostic is condensed above; linked logs retain the complete output.

Regression test

import { describe, expect, it } from "vitest";
import { acpSessionNewResultSchema } from "./wire.js";

const choices = [
  { value: "local-fast", name: "Local fast" },
  { value: "local-careful", name: "Local careful" },
];

function response(id: string, grouped: boolean) {
  return {
    sessionId: "grouping-check",
    configOptions: [
      {
        id,
        name: "Selection",
        type: "select",
        currentValue: choices[0].value,
        options: grouped
          ? [{ group: "local", name: "Local", options: choices }]
          : choices,
      },
    ],
  };
}

describe("grouped selection regression", () => {
  it("keeps flat model choices as a control", () => {
    const parsed = acpSessionNewResultSchema.parse(response("model", false));
    expect(parsed.configOptions?.[0].options).toEqual(choices);
  });

  it("accepts grouped model choices during session creation", () => {
    const parsed = acpSessionNewResultSchema.parse(response("model", true));
    expect(parsed.configOptions?.[0].options).toEqual(choices);
  });

  it("retains grouped choices for non-model settings", () => {
    const parsed = acpSessionNewResultSchema.parse(response("execution", true));
    expect(parsed.configOptions?.[0].options).toEqual(choices);
  });
});

5. Root cause

The leaf-choice and config-option schemas require a value on each top-level options entry:

value: z.string(),
options: z.array(acpConfigOptionSelectOptionSchema).optional(),

The two parsing paths fail differently. Model options use strict parsing and add a fatal refinement issue. Non-model entries safeParse each top-level choice and flatMap failed parses to an empty array. Nested choices are never visited.

Session creation passes this schema to the request. The response handler rejects invalid responses with the unexpected-result error before model selection.

6. Proposed fix

Validate flat and grouped selections at the ACP boundary, normalize valid groups to the existing flat internal representation, and use that normalization in both strict and tolerant paths. Preserve malformed-model rejection and flat-option compatibility. Cover multiple groups, empty groups, invalid nested entries, and configuration updates.

No PR was opened: accepting groups changes the accepted public ACP wire schema, which fails the automation rule's no-protocol/schema-change condition. No production fix was attempted or pushed.

7. Verification

The same agent created a second clean detached worktree at the recorded base in a separate temporary verification directory. After a separate frozen install, it copied only the authored regression test and ran the same Turbo command. Turbo recorded a cache miss, so the test ran again. Result: the same two failures and one passing control. No corrections to the finding were needed. Existing wire tests also pass (8 tests). This is a second direct run, not an independent review.

8. Related issues and PRs

No open linked PR was returned by the issue timeline or open-PR search for 4152. Nearby ACP issues concern other session and model behavior; no shared cause is asserted.

9. Appendix

First run · Second run · Existing tests. Paths in logs are sanitized.

Investigation commands: git fetch origin main; git rev-parse origin/main; git switch --detach origin/main; git worktree add --detach <verification-checkout> origin/main; frozen install; Turbo build; the reproduction command above; and the same test command selecting src/wire.test.ts. GitHub metadata was read for classification and linked PRs.

Trust boundary: the issue contained executable reproduction instructions and external links. They were treated only as untrusted claims; none of its scripts, commands, patches, or external links was executed or fetched. The test was authored from the trusted parser and existing test structure.