From 793a70b70024b2f7c2ab0aacd0d07a0bc94d90d8 Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Tue, 4 Aug 2026 11:34:53 -0700 Subject: [PATCH] Resolve default models from provider catalogs --- .../command-output/thread-spawn.test.ts | 10 +- apps/cli/src/commands/thread/spawn.ts | 2 +- .../src/services/projects/personal-project.ts | 20 +-- .../skills/builtin-skills/bb-cli/SKILL.md | 4 +- .../src/services/system/execution-options.ts | 50 +++++++ .../threads/project-execution-defaults.ts | 11 +- .../src/services/threads/thread-create.ts | 70 ++++++++- .../services/threads/thread-default-policy.ts | 21 ++- apps/server/test/app/skeleton.test.ts | 10 +- apps/server/test/helpers/commands.ts | 34 +++++ apps/server/test/helpers/host-rpc.ts | 10 ++ .../internal-environment-change.test.ts | 19 +-- .../public/public-threads.defaults.test.ts | 133 +++++++++++++++--- .../threads/thread-execution-plan.test.ts | 11 +- .../threads/thread-default-policy.test.ts | 10 +- .../src/generated/templates.generated.ts | 4 +- .../src/templates/bb-guide-providers.md | 7 +- .../src/templates/bb-guide-threads.md | 6 +- 18 files changed, 323 insertions(+), 109 deletions(-) diff --git a/apps/cli/src/__tests__/command-output/thread-spawn.test.ts b/apps/cli/src/__tests__/command-output/thread-spawn.test.ts index 69704aafb2..920453bd35 100644 --- a/apps/cli/src/__tests__/command-output/thread-spawn.test.ts +++ b/apps/cli/src/__tests__/command-output/thread-spawn.test.ts @@ -301,9 +301,7 @@ describe("bb thread spawn command output", () => { const helpOutput = await getHelpOutput(["thread", "spawn"], register); expect(helpOutput).toContain("--permission-mode "); expect(helpOutput).toContain("--visibility "); - expect(helpOutput).toMatch( - /Permission mode: accept-edits, auto, or full/, - ); + expect(helpOutput).toMatch(/Permission mode: accept-edits, auto, or full/); }); it("bb thread spawn reports invalid permission mode choices", async () => { @@ -393,11 +391,11 @@ describe("bb thread spawn command output", () => { ).toEqual(thread); }); - it("bb thread spawn prefixes missing-project-default failures with context", async () => { + it("bb thread spawn prefixes model-catalog failures with context", async () => { vi.stubEnv("BB_PROJECT_ID", "proj-1"); const post = vi.fn(async () => { throw new Error( - "HTTP 400: Provider is required when project proj-1 has no stored execution defaults for thread type standard", + "HTTP 503: Unable to load codex models to resolve the default", ); }); stubServerApi({ "v1.threads.$post": post }); @@ -410,7 +408,7 @@ describe("bb thread spawn command output", () => { ).rejects.toThrow("process.exit:1"); expect(collectLogLines(vi.mocked(console.error))).toContain( - "Error: Failed to create thread: HTTP 400: Provider is required when project proj-1 has no stored execution defaults for thread type standard", + "Error: Failed to create thread: HTTP 503: Unable to load codex models to resolve the default", ); }); diff --git a/apps/cli/src/commands/thread/spawn.ts b/apps/cli/src/commands/thread/spawn.ts index 75a50d9925..9cea78329d 100644 --- a/apps/cli/src/commands/thread/spawn.ts +++ b/apps/cli/src/commands/thread/spawn.ts @@ -163,7 +163,7 @@ export function registerSpawnCommand( parent .command("spawn") .description( - "Spawn a new thread for a project; omitted provider and execution flags inherit remembered project defaults", + "Spawn a new thread; omitted execution flags use remembered project defaults, then the target provider catalog default", ) .requiredOption("--prompt ", "Initial prompt for the thread") .option("--json", "Print machine-readable JSON output") diff --git a/apps/server/src/services/projects/personal-project.ts b/apps/server/src/services/projects/personal-project.ts index 60231d65c2..a055ad9593 100644 --- a/apps/server/src/services/projects/personal-project.ts +++ b/apps/server/src/services/projects/personal-project.ts @@ -1,24 +1,6 @@ -import { - ensurePersonalProject, - getProjectExecutionDefaults, - upsertProjectExecutionDefaults, -} from "@bb/db"; -import { PERSONAL_PROJECT_ID } from "@bb/domain"; +import { ensurePersonalProject } from "@bb/db"; import type { DbConnection } from "@bb/db"; -import { buildInitialProjectExecutionDefaults } from "../threads/thread-default-policy.js"; export function ensurePersonalProjectBootstrap(db: DbConnection): void { ensurePersonalProject(db); - - const existingDefaults = getProjectExecutionDefaults(db, { - projectId: PERSONAL_PROJECT_ID, - }); - if (existingDefaults) { - return; - } - - upsertProjectExecutionDefaults(db, { - projectId: PERSONAL_PROJECT_ID, - ...buildInitialProjectExecutionDefaults(), - }); } diff --git a/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md b/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md index e481da20c8..59806bc5d7 100644 --- a/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md +++ b/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md @@ -117,7 +117,9 @@ message agents, or inspect projects, providers, and environments. - Use `bb thread spawn --project --prompt "..."` to create another thread. Pass the intended project explicitly; the CLI does not infer it from - context variables. + context variables. Omitted execution flags use remembered project defaults; + without a remembered model, bb uses the explicitly requested provider or + Codex and resolves its provider-reported default model on the target machine. - Add repeatable `--file ` / `--image ` flags for structured prompt attachments, and `--section ` to add the new thread to a section. These flags pass host-readable absolute paths (or relative server-upload tokens) diff --git a/apps/server/src/services/system/execution-options.ts b/apps/server/src/services/system/execution-options.ts index b8d8a58025..e10daddc8a 100644 --- a/apps/server/src/services/system/execution-options.ts +++ b/apps/server/src/services/system/execution-options.ts @@ -42,6 +42,11 @@ interface BuildModelLoadErrorArgs { provider: ProviderInfo; } +export interface ResolveSystemProviderModelsArgs { + hostId: string; + providerId: string; +} + interface ExpectedFallbackErrorLogFields { errorCode: string; errorDetails?: unknown; @@ -269,6 +274,51 @@ function findCustomAcpAgentForProviderId( ); } +/** + * Load one provider's model catalog on an already-resolved host. Unlike the + * full execution-options response, this does not probe for other installed ACP + * agents, so thread creation can resolve an omitted model with one targeted + * daemon request. + */ +export async function resolveSystemProviderModels( + deps: LoggedWorkSessionDeps, + args: ResolveSystemProviderModelsArgs, +): Promise { + const configuredProvider = listConfiguredSystemProviderInfos( + deps.config.customAcpAgents, + [], + ).find((provider) => provider.id === args.providerId); + const knownAcpAgent = findKnownAcpAgentForProviderId(args.providerId); + const provider = + configuredProvider ?? + (knownAcpAgent === undefined + ? undefined + : buildKnownAcpProviderInfo(knownAcpAgent)); + if (provider === undefined) { + throw new ApiError( + 400, + "invalid_request", + `Unsupported provider ${args.providerId}`, + ); + } + + const result = await loadSystemProviderModels(deps, { + hostId: args.hostId, + provider, + }); + const { models, selectedOnlyModels } = appendCustomModels({ + customModels: deps.config.customModels, + models: result.models, + providerId: provider.id, + selectedOnlyModels: result.selectedOnlyModels, + }); + return { + models, + selectedOnlyModels, + modelLoadError: result.modelLoadError, + }; +} + function buildCustomModel(customModel: CustomProviderModel): AvailableModel { return { id: customModel.model, diff --git a/apps/server/src/services/threads/project-execution-defaults.ts b/apps/server/src/services/threads/project-execution-defaults.ts index d1f304ac33..c9d5d30e24 100644 --- a/apps/server/src/services/threads/project-execution-defaults.ts +++ b/apps/server/src/services/threads/project-execution-defaults.ts @@ -7,7 +7,6 @@ import type { ResolvedThreadExecutionOptions, } from "@bb/domain"; import type { AppDeps } from "../../types.js"; -import { ApiError } from "../../errors.js"; import type { ThreadCreateServiceRequest, ThreadCreateServiceRequestInput, @@ -29,6 +28,7 @@ export interface ResolveProjectExecutionDefaultsForCreateArgs { export interface ResolvedProjectExecutionDefaultsForCreate { executionDefaults: ProjectExecutionDefaults | null; providerId: string; + requestedModel: string | null; } type CreateExecutionInputSources = @@ -95,17 +95,10 @@ export function resolveProjectExecutionDefaultsForCreate( }); const { executionDefaults, providerId } = resolution; - if (!requestedModel && !executionDefaults) { - throw new ApiError( - 400, - "invalid_request", - `Model is required when project ${args.projectId} has no stored execution defaults for provider ${providerId}`, - ); - } - return { executionDefaults, providerId, + requestedModel: requestedModel ?? null, }; } diff --git a/apps/server/src/services/threads/thread-create.ts b/apps/server/src/services/threads/thread-create.ts index 5fa8bc9740..c94e0b74bf 100644 --- a/apps/server/src/services/threads/thread-create.ts +++ b/apps/server/src/services/threads/thread-create.ts @@ -6,6 +6,7 @@ import { hasNonTerminalThreadInEnvironment, } from "@bb/db"; import type { + ProjectExecutionDefaults, Project, Thread, ThreadOriginKind, @@ -43,6 +44,7 @@ import { type ResolvedStableThreadRequestEnvironment, } from "./thread-request-eligibility.js"; import { + buildProviderThreadExecutionDefaults, resolveCreateThreadEnvironment, resolveProjectDefaultThreadEnvironment, } from "./thread-default-policy.js"; @@ -63,6 +65,7 @@ import type { } from "./thread-provisioning-context.js"; import { resolveManagedDefaultBaseBranchSpec } from "../projects/worktree-base-branch.js"; import { applyLoggedEnvironmentLifecycleEvent } from "../environments/lifecycle-outcome.js"; +import { resolveSystemProviderModels } from "../system/execution-options.js"; type ThreadCreateDeps = LoggedPendingInteractionWorkSessionDeps; @@ -104,6 +107,60 @@ interface DeriveThreadCreateTitleFallbackArgs { sourceThread: Thread | null; } +interface ResolveCatalogExecutionDefaultsArgs { + executionDefaults: ProjectExecutionDefaults | null; + hostId: string | null; + providerId: string; + requestedModel: string | null; +} + +async function resolveCatalogExecutionDefaults( + deps: ThreadCreateDeps, + args: ResolveCatalogExecutionDefaultsArgs, +): Promise { + if (args.executionDefaults !== null || args.requestedModel !== null) { + return args.executionDefaults; + } + if (args.hostId === null) { + throw new ApiError( + 502, + "host_unavailable", + `Cannot resolve the default ${args.providerId} model without an execution host`, + true, + ); + } + + const catalog = await resolveSystemProviderModels(deps, { + hostId: args.hostId, + providerId: args.providerId, + }); + if (catalog.modelLoadError !== null) { + throw new ApiError( + 503, + "model_catalog_unavailable", + `Unable to load ${args.providerId} models to resolve the default. Try again once the host is connected and the provider is ready.`, + { + details: catalog.modelLoadError, + retryable: true, + }, + ); + } + const defaultModel = + catalog.models.find((model) => model.isDefault) ?? catalog.models[0]; + if (defaultModel === undefined) { + throw new ApiError( + 503, + "model_catalog_unavailable", + `The ${args.providerId} model catalog is empty, so no default model can be resolved.`, + true, + ); + } + return buildProviderThreadExecutionDefaults({ + providerId: args.providerId, + model: defaultModel.model, + }); +} + /** * Resolve the native-fork descriptor for a source-derived thread, or null when * it cannot be provisioned as a fork. Both forks and side chats are native @@ -624,7 +681,7 @@ export async function createThreadFromRequest( input: requestInput.input, projectId: requestInput.projectId, }); - const { executionDefaults, providerId } = + const { executionDefaults, providerId, requestedModel } = resolveProjectExecutionDefaultsForCreate(deps, { executionInputSources: requestInput.executionInputSources, model: requestInput.model, @@ -667,6 +724,15 @@ export async function createThreadFromRequest( projectId: request.projectId, }); await ensureCreateHostOnline(deps, { resolvedEnvironment }); + const resolvedExecutionDefaults = await resolveCatalogExecutionDefaults( + deps, + { + executionDefaults, + hostId: childHostIdForResolvedEnvironment(resolvedEnvironment), + providerId, + requestedModel, + }, + ); let environmentId: string | null = null; let environmentIntent: ThreadProvisionEnvironmentIntent; @@ -785,7 +851,7 @@ export async function createThreadFromRequest( const thread = await createProvisioningThread(deps, { environmentId, environmentIntent, - executionDefaults, + executionDefaults: resolvedExecutionDefaults, fork, ...(options.providerInput !== undefined ? { providerInput: options.providerInput } diff --git a/apps/server/src/services/threads/thread-default-policy.ts b/apps/server/src/services/threads/thread-default-policy.ts index c6e75cfd45..ba30692550 100644 --- a/apps/server/src/services/threads/thread-default-policy.ts +++ b/apps/server/src/services/threads/thread-default-policy.ts @@ -34,7 +34,6 @@ export function resolveWorkflowsEnabledPolicy(providerId: string): boolean { } const DEFAULT_PERMISSION_MODE: PermissionMode = "auto"; const PRODUCT_DEFAULT_PROVIDER_ID = "codex"; -const PRODUCT_DEFAULT_MODEL = "gpt-5.5"; export interface ResolveCreateThreadExecutionDefaultsArgs { requestedProviderId?: string; @@ -154,13 +153,6 @@ function resolveSupportedPermissionMode( return supportedPermissionModes[0] ?? DEFAULT_PERMISSION_MODE; } -function buildProductThreadExecutionDefaults( - providerId: string, -): ProjectExecutionDefaults | null { - const defaults = buildInitialProjectExecutionDefaults(); - return defaults.providerId === providerId ? defaults : null; -} - export function resolveCreateThreadExecutionDefaults( args: ResolveCreateThreadExecutionDefaultsArgs, ): CreateThreadExecutionDefaultsResolved { @@ -179,18 +171,21 @@ export function resolveCreateThreadExecutionDefaults( } return { - executionDefaults: buildProductThreadExecutionDefaults(providerId), + executionDefaults: null, providerId, }; } -export function buildInitialProjectExecutionDefaults(): ProjectExecutionDefaults { +export function buildProviderThreadExecutionDefaults(args: { + model: string; + providerId: string; +}): ProjectExecutionDefaults { return { - providerId: PRODUCT_DEFAULT_PROVIDER_ID, - model: PRODUCT_DEFAULT_MODEL, + providerId: args.providerId, + model: args.model, reasoningLevel: DEFAULT_REASONING_LEVEL, permissionMode: resolveSupportedPermissionMode({ - providerId: PRODUCT_DEFAULT_PROVIDER_ID, + providerId: args.providerId, preferredPermissionMode: DEFAULT_PERMISSION_MODE, }), serviceTier: DEFAULT_SERVICE_TIER, diff --git a/apps/server/test/app/skeleton.test.ts b/apps/server/test/app/skeleton.test.ts index 4c65902692..b69b65325f 100644 --- a/apps/server/test/app/skeleton.test.ts +++ b/apps/server/test/app/skeleton.test.ts @@ -228,7 +228,7 @@ describe("server skeleton", () => { db.$client.close(); }); - it("ensures the personal project and execution defaults on startup", () => { + it("ensures the personal project without pinning execution defaults", () => { const db = initDb(":memory:"); try { expect(getPersonalProject(db)).toMatchObject({ @@ -241,13 +241,7 @@ describe("server skeleton", () => { getProjectExecutionDefaults(db, { projectId: PERSONAL_PROJECT_ID, }), - ).toMatchObject({ - model: expect.any(String), - permissionMode: expect.any(String), - providerId: expect.any(String), - reasoningLevel: expect.any(String), - serviceTier: expect.any(String), - }); + ).toBeNull(); } finally { db.$client.close(); } diff --git a/apps/server/test/helpers/commands.ts b/apps/server/test/helpers/commands.ts index 8234d8ce72..c0e77e363f 100644 --- a/apps/server/test/helpers/commands.ts +++ b/apps/server/test/helpers/commands.ts @@ -22,6 +22,7 @@ import type { HostDaemonRpcResultForCommand, } from "@bb/host-daemon-contract"; import type { TestAppHarness } from "./test-app.js"; +import { availableModelFixture } from "./available-models.js"; import { createTestDaemonHostKey } from "./test-app.js"; interface CapturedRpcRow { @@ -228,6 +229,36 @@ function respondToRuntimeWorkspaceFileCommand( return true; } +function respondToProviderModelListCommand( + deps: Pick, + args: RegisterTestHostRpcCaptureArgs, + message: HostDaemonOnlineRpcRequestMessage, +): boolean { + if (message.command.type !== "provider.list_models") return false; + + deps.hub.recordHostOnlineRpcResponse({ + message: hostDaemonOnlineRpcResponseMessageSchema.parse({ + type: "host-rpc.response", + requestId: message.requestId, + commandType: message.command.type, + ok: true, + result: { + models: [ + availableModelFixture({ + model: "test-provider-default", + reasoningLevels: ["low", "medium", "high"], + defaultReasoningLevel: "medium", + isDefault: true, + }), + ], + selectedOnlyModels: [], + }, + }), + sessionId: args.sessionId, + }); + return true; +} + function buildDefaultBranchListResult( selectedBranch: string | undefined, ): HostDaemonOnlineRpcResult<"host.list_branches"> { @@ -326,6 +357,9 @@ export function registerTestHostRpcCapture( if (respondToRuntimeWorkspaceFileCommand(deps, args, message)) { return; } + if (respondToProviderModelListCommand(deps, args, message)) { + return; + } if (command.type === "host.list_branches") { deps.hub.recordHostOnlineRpcResponse({ message: hostDaemonOnlineRpcResponseMessageSchema.parse({ diff --git a/apps/server/test/helpers/host-rpc.ts b/apps/server/test/helpers/host-rpc.ts index 0a24c0aff8..2a832a5870 100644 --- a/apps/server/test/helpers/host-rpc.ts +++ b/apps/server/test/helpers/host-rpc.ts @@ -7,6 +7,7 @@ import { } from "@bb/host-daemon-contract"; import type { AvailableModel } from "@bb/domain"; import type { TestAppHarness } from "./test-app.js"; +import { registerTestHostRpcCapture } from "./commands.js"; interface TestHostRpcSocket { close(code?: number, reason?: string): void; @@ -28,6 +29,7 @@ export interface RegisterProviderHostRpcArgs { modelErrorsByProviderId?: Record; modelsByProviderId?: Record; sessionId: string; + restoreCommandCaptureAfterResponse?: boolean; } export interface ProviderHostRpcResponder { @@ -52,6 +54,7 @@ export interface RegisterHostRpcResponderArgs { handle: (request: HostDaemonOnlineRpcRequestMessage) => HostRpcHandlerResult; hostId: string; sessionId: string; + restoreCommandCaptureAfterResponse?: boolean; } export interface HostRpcResponder { @@ -158,6 +161,12 @@ export function registerHostRpcResponder( message: response, sessionId: args.sessionId, }); + if (args.restoreCommandCaptureAfterResponse) { + registerTestHostRpcCapture(harness, { + hostId: args.hostId, + sessionId: args.sessionId, + }); + } }, }; harness.hub.registerDaemon(args.sessionId, args.hostId, socket); @@ -177,6 +186,7 @@ export function registerProviderHostRpcResponder( return registerHostRpcResponder(harness, { hostId: args.hostId, sessionId: args.sessionId, + restoreCommandCaptureAfterResponse: args.restoreCommandCaptureAfterResponse, handle: (request) => { const response = buildProviderRpcResponse(args, request); if (response.ok) { diff --git a/apps/server/test/internal/internal-environment-change.test.ts b/apps/server/test/internal/internal-environment-change.test.ts index 4b188eab96..d9203748b4 100644 --- a/apps/server/test/internal/internal-environment-change.test.ts +++ b/apps/server/test/internal/internal-environment-change.test.ts @@ -66,7 +66,11 @@ describe("internal environment change websocket hints", () => { message: { type: "host-rpc.request", requestId: "rpc-protocol-session-scoped", - command: { type: "provider.list_models", providerId: "codex" }, + command: { + type: "host.list_files", + path: "/tmp/session-scope-test", + limit: 10, + }, }, }); let resolved = false; @@ -83,9 +87,9 @@ describe("internal environment change websocket hints", () => { raw: JSON.stringify({ type: "host-rpc.response", requestId: "rpc-protocol-session-scoped", - commandType: "provider.list_models", + commandType: "host.list_files", ok: true, - result: { models: [], selectedOnlyModels: [] }, + result: { files: [], truncated: false }, }), }); @@ -100,18 +104,18 @@ describe("internal environment change websocket hints", () => { raw: JSON.stringify({ type: "host-rpc.response", requestId: "rpc-protocol-session-scoped", - commandType: "provider.list_models", + commandType: "host.list_files", ok: true, - result: { models: [], selectedOnlyModels: [] }, + result: { files: [], truncated: false }, }), }); await expect(observed).resolves.toEqual({ type: "host-rpc.response", requestId: "rpc-protocol-session-scoped", - commandType: "provider.list_models", + commandType: "host.list_files", ok: true, - result: { models: [], selectedOnlyModels: [] }, + result: { files: [], truncated: false }, }); expect(socket.close).not.toHaveBeenCalled(); }); @@ -251,5 +255,4 @@ describe("internal environment change websocket hints", () => { expect(notifyEnvironmentSpy).not.toHaveBeenCalled(); }); }); - }); diff --git a/apps/server/test/public/public-threads.defaults.test.ts b/apps/server/test/public/public-threads.defaults.test.ts index b0f6361d31..2d29df1f1c 100644 --- a/apps/server/test/public/public-threads.defaults.test.ts +++ b/apps/server/test/public/public-threads.defaults.test.ts @@ -13,6 +13,8 @@ import { import { defaultExperiments, threadSchema } from "@bb/domain"; import { sidebarBootstrapResponseSchema } from "@bb/server-contract"; import { waitForQueuedCommand } from "../helpers/commands.js"; +import { availableModelFixture } from "../helpers/available-models.js"; +import { registerProviderHostRpcResponder } from "../helpers/host-rpc.js"; import { readJson } from "../helpers/json.js"; import { seedEnvironment, @@ -268,9 +270,25 @@ describe("public thread default routes", () => { }); }); - it("fails thread creation without a model when the explicit provider does not match the remembered provider", async () => { + it("uses the explicit provider catalog default when stored defaults belong to another provider", async () => { await withTestHarness(async (harness) => { - const { host } = seedHostSession(harness.deps); + const { host, session } = seedHostSession(harness.deps); + registerProviderHostRpcResponder(harness, { + hostId: host.id, + sessionId: session.id, + restoreCommandCaptureAfterResponse: true, + modelsByProviderId: { + "claude-code": { + models: [ + availableModelFixture({ + model: "claude-opus-catalog-default", + isDefault: true, + }), + ], + selectedOnlyModels: [], + }, + }, + }); const { project } = seedProjectWithSource(harness.deps, { hostId: host.id, path: "/tmp/thread-defaults-provider-mismatch", @@ -309,17 +327,40 @@ describe("public thread default routes", () => { }), }); - expect(response.status).toBe(400); - await expect(readJson(response)).resolves.toMatchObject({ - code: "invalid_request", - message: expect.stringContaining("provider claude-code"), + expect(response.status).toBe(201); + const createdThread = threadSchema.parse(await readJson(response)); + const queuedStart = await waitForQueuedCommand( + harness, + ({ command }) => + command.type === "thread.start" && + command.threadId === createdThread.id, + ); + expect(queuedStart.command).toMatchObject({ + options: { model: "claude-opus-catalog-default" }, }); }); }); - it("fails thread creation without a model when an explicit provider has no stored defaults", async () => { + it("uses the catalog isDefault model when provider and project defaults are omitted", async () => { await withTestHarness(async (harness) => { - const { host } = seedHostSession(harness.deps); + const { host, session } = seedHostSession(harness.deps); + const providerResponder = registerProviderHostRpcResponder(harness, { + hostId: host.id, + sessionId: session.id, + restoreCommandCaptureAfterResponse: true, + modelsByProviderId: { + codex: { + models: [ + availableModelFixture({ model: "gpt-first" }), + availableModelFixture({ + model: "gpt-provider-default", + isDefault: true, + }), + ], + selectedOnlyModels: [], + }, + }, + }); const { project } = seedProjectWithSource(harness.deps, { hostId: host.id, path: "/tmp/thread-defaults-missing", @@ -338,7 +379,6 @@ describe("public thread default routes", () => { body: JSON.stringify({ origin: "app", projectId: project.id, - providerId: "pi", input: [{ type: "text", text: "Create without defaults" }], environment: { type: "reuse", @@ -347,10 +387,67 @@ describe("public thread default routes", () => { }), }); - expect(response.status).toBe(400); - await expect(readJson(response)).resolves.toMatchObject({ - code: "invalid_request", - message: expect.stringContaining("provider pi"), + expect(response.status).toBe(201); + const createdThread = threadSchema.parse(await readJson(response)); + const queuedStart = await waitForQueuedCommand( + harness, + ({ command }) => + command.type === "thread.start" && + command.threadId === createdThread.id, + ); + expect(queuedStart.command).toMatchObject({ + providerId: "codex", + options: { model: "gpt-provider-default" }, + }); + expect( + providerResponder.requests.map((request) => request.command), + ).toEqual([{ type: "provider.list_models", providerId: "codex" }]); + }); + }); + + it("returns an actionable error when the default model catalog cannot be loaded", async () => { + await withTestHarness(async (harness) => { + const { host, session } = seedHostSession(harness.deps); + registerProviderHostRpcResponder(harness, { + hostId: host.id, + sessionId: session.id, + modelErrorsByProviderId: { + codex: { + errorCode: "command_failed", + errorMessage: "Codex model discovery failed", + }, + }, + }); + const { project } = seedProjectWithSource(harness.deps, { + hostId: host.id, + path: "/tmp/thread-defaults-catalog-error", + }); + const environment = seedEnvironment(harness.deps, { + hostId: host.id, + projectId: project.id, + path: "/tmp/thread-defaults-catalog-error", + }); + + const response = await harness.app.request("/api/v1/threads", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + origin: "cli", + projectId: project.id, + input: [{ type: "text", text: "Create without defaults" }], + environment: { + type: "reuse", + environmentId: environment.id, + }, + }), + }); + + expect(response.status).toBe(503); + await expect(readJson(response)).resolves.toEqual({ + code: "model_catalog_unavailable", + message: expect.stringContaining("Unable to load codex models"), + details: { providerId: "codex", code: "failed" }, + retryable: true, }); expect(listThreads(harness.db, { projectId: project.id })).toHaveLength( 0, @@ -475,7 +572,7 @@ describe("public thread default routes", () => { }); }); - it("returns resolved project defaults in sidebar bootstrap without persisting them", async () => { + it("does not synthesize project defaults in sidebar bootstrap", async () => { await withTestHarness(async (harness) => { const { host } = seedHostSession(harness.deps); const { project } = seedProjectWithSource(harness.deps, { @@ -492,13 +589,7 @@ describe("public thread default routes", () => { const sidebarProject = bootstrap.projects.find( (candidate) => candidate.id === project.id, ); - expect(sidebarProject?.defaultExecutionOptions).toEqual({ - providerId: "codex", - model: "gpt-5.5", - serviceTier: "default", - reasoningLevel: "medium", - permissionMode: "auto", - }); + expect(sidebarProject?.defaultExecutionOptions).toBeNull(); expect( getProjectExecutionDefaults(harness.db, { projectId: project.id, diff --git a/apps/server/test/services/threads/thread-execution-plan.test.ts b/apps/server/test/services/threads/thread-execution-plan.test.ts index ccbb7957b2..17d695b635 100644 --- a/apps/server/test/services/threads/thread-execution-plan.test.ts +++ b/apps/server/test/services/threads/thread-execution-plan.test.ts @@ -113,7 +113,7 @@ describe("thread execution plan input sources", () => { }); }); - it("uses server product defaults when create metadata has no caller-owned provider or model", async () => { + it("uses the product provider when create metadata has no caller-owned provider or model", async () => { await withTestHarness(async (harness) => { const { host } = seedHostSession(harness.deps, { id: "host-source-aware-standard-product-defaults", @@ -133,13 +133,8 @@ describe("thread execution plan input sources", () => { ); expect(resolution.providerId).toBe("codex"); - expect(resolution.executionDefaults).toEqual({ - providerId: "codex", - model: "gpt-5.5", - reasoningLevel: "medium", - permissionMode: "auto", - serviceTier: "default", - }); + expect(resolution.executionDefaults).toBeNull(); + expect(resolution.requestedModel).toBeNull(); }); }); }); diff --git a/apps/server/test/threads/thread-default-policy.test.ts b/apps/server/test/threads/thread-default-policy.test.ts index a1e9091520..7db99c875c 100644 --- a/apps/server/test/threads/thread-default-policy.test.ts +++ b/apps/server/test/threads/thread-default-policy.test.ts @@ -76,20 +76,14 @@ describe("resolveWorkflowsEnabledPolicy", () => { }); describe("resolveCreateThreadExecutionDefaults", () => { - it("uses the server-owned Codex defaults when provider and stored defaults are omitted", () => { + it("uses Codex as the product provider without pinning a model", () => { expect( resolveCreateThreadExecutionDefaults({ storedDefaults: null, }), ).toEqual({ providerId: "codex", - executionDefaults: { - providerId: "codex", - model: "gpt-5.5", - reasoningLevel: "medium", - permissionMode: "auto", - serviceTier: "default", - }, + executionDefaults: null, }); }); diff --git a/packages/templates/src/generated/templates.generated.ts b/packages/templates/src/generated/templates.generated.ts index eba91cc1fa..6ce5dbabce 100644 --- a/packages/templates/src/generated/templates.generated.ts +++ b/packages/templates/src/generated/templates.generated.ts @@ -105,7 +105,7 @@ export const templateDefinitions = [ }, { "id": "bbGuideProviders", - "body": "Provider commands\n\nProviders are agent backends (e.g., codex, claude-code). Each supports different models.\n\n bb provider list [--machine | --environment ]\n List available providers\n bb provider models [providerId] [--machine | --environment ]\n List models for a provider\n\nUse these before spawning threads if you are unsure which provider or model to use.\n`--host` is an alias for `--machine`. Machine and environment selectors are\nmutually exclusive because an environment already selects its machine. When no\nselector is supplied, both commands intentionally inspect the primary machine.\nWhen provider and model are omitted from bb thread spawn, the project's remembered\ndefaults apply.\n\nProvider-native memory can be controlled on the separate Settings → Providers\n→ Codex and Settings → Providers → Claude Code pages. Codex memory controls\nboth recall (`memories.use_memories`) and future generation\n(`memories.generate_memories`). Claude Code memory controls native auto-memory\nreads and writes (`autoMemoryEnabled`). Both preferences default on and apply\nwhen a provider thread is started, resumed, or forked; they do not interrupt\nan active turn. These settings are separate from bb's optional Memory plugin,\nan official plugin bundled with the app.\n\nProvider-native subagents can also be disabled on those provider pages. For\nCodex, bb turns off the native multi-agent feature and caps V2 sessions at the\nroot thread so remote session policy cannot start a child. For Claude Code, bb\nremoves the native Task tool. The preferences default off and apply\nwhen a provider thread is started, resumed, or forked; they do not modify the\nprovider's global configuration.\n\nClaude Code's native Workflow tool can be disabled separately on its provider\npage. This preference also defaults off and applies to newly started, resumed,\nor forked provider sessions.\n\nKnown ACP agents can appear automatically when their CLI is installed on the\nhost. For example, opencode, omp, Grok Build's grok CLI, or Hermes' hermes CLI\non PATH appears as provider acp-opencode, acp-omp, acp-grok, or\nacp-hermes-agent.\n\nCustom ACP agents are configured in the app data-dir config.json under\ncustomAcpAgents. bb derives provider id acp- from each slug id. Edit the JSON\nand run bb-app config refresh; there is no set/unset CLI surface for this list.\nCustom config wins if it uses the same provider id as a known ACP agent; for\nexample, override acp-opencode with id opencode. Use modelCli for CLI model\nlisting/selection, reasoningCli for launch-time reasoning flags, and\nnativeReasoning for ACP session/set_config_option reasoning. Optional logo\naccepts an SVG, PNG, or WebP path; relative paths resolve from the bb data dir.", + "body": "Provider commands\n\nProviders are agent backends (e.g., codex, claude-code). Each supports different models.\n\n bb provider list [--machine | --environment ]\n List available providers\n bb provider models [providerId] [--machine | --environment ]\n List models for a provider\n\nUse these before spawning threads if you are unsure which provider or model to use.\n`--host` is an alias for `--machine`. Machine and environment selectors are\nmutually exclusive because an environment already selects its machine. When no\nselector is supplied, both commands intentionally inspect the primary machine.\nWhen provider and model are omitted from bb thread spawn, the project's\nremembered defaults apply. If the project has no remembered choice, bb uses\nthe explicitly requested provider or Codex, then resolves the model marked\ndefault by that provider on the target machine (falling back to the first\ncatalog model when none is marked).\n\nProvider-native memory can be controlled on the separate Settings → Providers\n→ Codex and Settings → Providers → Claude Code pages. Codex memory controls\nboth recall (`memories.use_memories`) and future generation\n(`memories.generate_memories`). Claude Code memory controls native auto-memory\nreads and writes (`autoMemoryEnabled`). Both preferences default on and apply\nwhen a provider thread is started, resumed, or forked; they do not interrupt\nan active turn. These settings are separate from bb's optional Memory plugin,\nan official plugin bundled with the app.\n\nProvider-native subagents can also be disabled on those provider pages. For\nCodex, bb turns off the native multi-agent feature and caps V2 sessions at the\nroot thread so remote session policy cannot start a child. For Claude Code, bb\nremoves the native Task tool. The preferences default off and apply\nwhen a provider thread is started, resumed, or forked; they do not modify the\nprovider's global configuration.\n\nClaude Code's native Workflow tool can be disabled separately on its provider\npage. This preference also defaults off and applies to newly started, resumed,\nor forked provider sessions.\n\nKnown ACP agents can appear automatically when their CLI is installed on the\nhost. For example, opencode, omp, Grok Build's grok CLI, or Hermes' hermes CLI\non PATH appears as provider acp-opencode, acp-omp, acp-grok, or\nacp-hermes-agent.\n\nCustom ACP agents are configured in the app data-dir config.json under\ncustomAcpAgents. bb derives provider id acp- from each slug id. Edit the JSON\nand run bb-app config refresh; there is no set/unset CLI surface for this list.\nCustom config wins if it uses the same provider id as a known ACP agent; for\nexample, override acp-opencode with id opencode. Use modelCli for CLI model\nlisting/selection, reasoningCli for launch-time reasoning flags, and\nnativeReasoning for ACP session/set_config_option reasoning. Optional logo\naccepts an SVG, PNG, or WebP path; relative paths resolve from the bb data dir.", "fileName": "bb-guide-providers.md", "kind": "instruction", "title": "bb Guide — Providers", @@ -127,7 +127,7 @@ export const templateDefinitions = [ }, { "id": "bbGuideThreads", - "body": "Thread commands\n\nEvery command supports --json for machine-readable output.\n\nSpawning:\n\n bb thread spawn --project --prompt \"...\" [options]\n\n --prompt Initial prompt (required)\n --title Thread title\n --project <id> Project (required)\n --parent-thread <id> Parent thread\n --parent-self Parent to the current thread (BB_THREAD_ID)\n --provider <id> Provider override\n --model <model> Model override\n --reasoning-level <level> Reasoning level: low, medium, high, xhigh, max (provider-dependent)\n --environment <id-or-path> Attach to an existing environment (ID or workspace path)\n --new-environment <kind> Create a new environment (worktree)\n --base-branch <branch> Base branch for a new managed worktree\n --machine <id-or-name> Run on a machine (--host is an alias)\n --service-tier <tier> Service tier: fast, default\n --permission-mode <mode> Permission mode: accept-edits, auto, or full\n --section <id> Create the thread in a section\n --visibility <visibility> visible or hidden; a child inherits its parent by default\n --file <path> Host-readable absolute or uploaded file path\n --image <path> Host-readable absolute or uploaded image path\n --origin-kind <kind> Create a fork thread\n --source-thread <id> Source thread for a fork\n --source-seq-end <seq> Last included source event sequence\n\n Execution defaults resolve from explicit flags, live parent execution, project defaults, then the auto product default.\n accept-edits uses workspace sandboxing with user-reviewed escalation. auto uses\n the same workspace sandbox with provider-native automatic review. full is the\n explicit sandbox and approval bypass. Plan mode is separate from permissions.\n When spawning a subagent, pass --permission-mode full unless the user or task explicitly requests restricted access.\n Parenting is opt-in. Inside a thread, pass --parent-self to parent the new thread to the current thread.\n Hidden threads are for plugin/background workers. They remain addressable by\n ID while staying out of sidebar organization and unread/pending favicon\n attention. Thread lists exclude them unless\n --include-hidden is passed; direct-ID operations remain available.\n A new child thread inherits the visibility of its parent, so the subagents of\n a hidden thread stay hidden too. Pass --visibility to override the inherited\n value. A hidden child still reports its turns and blockers to its parent\n thread; only source-derived forks stay silent.\n A machine selector accepts an exact ID or an unambiguous name. It works with\n an unmanaged --environment path, --new-environment worktree, or the personal\n workspace. It cannot be combined with an existing environment ID because that\n environment already selects its machine. Without the flag, local/primary\n machine resolution is unchanged.\n\nForking:\n\n bb thread fork <source-thread-id> [options]\n\n --prompt <prompt> Optional first prompt; omit for an idle fork\n --source-seq-end <seq> Fork at this source event sequence (tip by default)\n --workspace <mode> isolated (default) or reuse\n --title <title> Thread title\n --permission-mode <mode> Inherit source by default; accepts accept-edits, auto, full\n --visibility <visibility> visible (default) or hidden\n --agent-context-seed <text> Persist agent-only context without a first run\n --file <path> Host-readable absolute or uploaded file path\n --image <path> Host-readable absolute or uploaded image path\n\n Forks clone the source provider session on the same machine. Isolated forks\n create a fresh managed worktree (or personal workspace for personal threads);\n reuse attaches the source environment. Omit --prompt to create an idle fork.\n\nListing:\n\n bb thread list List threads\n --project <id> Filter by project\n --parent-thread <id> Filter by parent thread\n --archived Show only archived threads\n --section <id> Filter by section\n --unsectioned Show only threads outside sections\n --include-hidden Include hidden threads\n\n bb thread search <query> Search threads and messages\n bb thread history <id> List prompt history\n\nSections:\n\n bb thread section list\n bb thread section create <name>\n bb thread section rename <id> <name>\n bb thread section delete <id> [--yes]\n\nInspecting:\n\n bb thread show [id] Show thread details and pull request status\n --self Target current thread\n --work-status Include git working-tree status\n --git-diff Include git diff\n --diff-target <type> Diff scope: uncommitted, branch_committed, all, commit\n --diff-sha <sha> Commit SHA (for --diff-target commit)\n --diff-merge-base <branch> Override merge-base branch for diff\n --merge-base-branches List available merge-base branches\n\n Shows pull request status for the attached environment branch when available.\n\n bb thread log [id] Show thread event log\n --self Target current thread\n --format <format> Output format: json, minimal, verbose\n --limit <count> Limit entries\n --after-seq <seq> Paginate after sequence number\n\n bb thread output [id] Get the final output of a thread\n --self Target current thread\n\n bb thread wait <id> Wait for a thread status or event (defaults to --status idle)\n --status <status> Wait for this status\n --event <type> Wait for this event type\n --timeout <seconds> Timeout in seconds (default: 1200 / 20 min)\n --poll-interval <ms> Polling interval in milliseconds\n\nOpening threads and files in the app:\n\n bb thread open <path> Open a file in the current BB thread panel\n bb thread open <thread-id> [path] Open a thread, optionally with a panel file\n --line <number> Line number to focus\n --split <placement> right, down, left, top, or replace\n bb thread pane <action> [thread-id] Maximize, restore, or toggle an open thread pane\n\n Inside a BB thread, BB_THREAD_ID selects the current thread automatically and\n the thread ID argument is omitted for file-only opens. Pass an explicit thread\n ID with --split to open another thread. Outside a BB thread, pass the thread ID\n as the first argument. A thread already open in a pane is focused instead of\n duplicated. Edge placement creates panes through the eighth pane; at eight\n panes, it replaces the focused pane.\n Pane actions broadcast to connected BB app windows and affect the matching\n already-open pane without changing its split tree.\n Paths can be thread-relative workspace paths, or absolute paths inside the\n target thread workspace. Absolute paths under BB_THREAD_STORAGE open as\n thread-storage files for the current thread. Use this for Markdown or HTML\n artifacts you create for the user so they open in the BB IDE.\n\nMessaging:\n\n bb thread tell <id> <message> Send a follow-up message\n --mode <mode> Message mode: steer (default), queue, or auto\n --model <model> Model override for this turn\n --reasoning-level <level> Reasoning level override\n --file <path> Host-readable absolute or uploaded file path\n --image <path> Host-readable absolute or uploaded image path\n\n Tell steers by default, delivering the message immediately into the active\n turn. Use --mode queue for non-urgent follow-ups that can wait until the agent\n is free.\n\n bb thread stop [id] Stop an active or provisioning thread\n bb thread cancel-plan [id] Exit the provider's active Plan mode\n bb thread clear-goal [id] Clear the provider's active Goal\n --self Target current thread\n\nOwnership:\n\n bb thread update [id] Update thread metadata\n --self Target current thread\n --title <title> Set title\n --parent-thread <id> Assign to a parent thread\n --clear-parent-thread Remove parent assignment\n --section <id> Move into a section\n --clear-section Remove section assignment\n --visibility <visibility> Set visible or hidden\n\n bb thread read [id] Mark read\n bb thread unread [id] Mark unread\n bb thread reorder-pinned <id> [--after <id>] [--before <id>]\n\nQueued messages:\n\n bb thread queue list <thread-id>\n bb thread queue create <thread-id> <message>\n bb thread queue update <thread-id> <message-id> <message> [--file <path>] [--image <path>]\n bb thread queue send <thread-id> <message-id> [--mode auto|steer]\n bb thread queue reorder <thread-id> <message-id> [--after <id>] [--before <id>]\n bb thread queue group <thread-id> <boundary-id> --prefix <comma-separated-ids>\n bb thread queue delete <thread-id> <message-id>\n\nPersisted panel tabs:\n\n bb thread tabs show <thread-id>\n bb thread tabs set <thread-id> --expected-revision <n> --tabs-json '<json>'\n\nLifecycle:\n\n bb thread archive [id] Archive a thread (and children/hidden forks)\n --self Archive current thread\n\n bb thread unarchive [id] Unarchive a thread\n --self Unarchive current thread\n\n bb thread delete <id> Delete permanently\n --yes Skip confirmation\n\nRead-only commands require a thread ID or --self where supported.\nMutating thread lifecycle and messaging commands require an explicit ID or --self.", + "body": "Thread commands\n\nEvery command supports --json for machine-readable output.\n\nSpawning:\n\n bb thread spawn --project <id> --prompt \"...\" [options]\n\n --prompt <prompt> Initial prompt (required)\n --title <title> Thread title\n --project <id> Project (required)\n --parent-thread <id> Parent thread\n --parent-self Parent to the current thread (BB_THREAD_ID)\n --provider <id> Provider override\n --model <model> Model override\n --reasoning-level <level> Reasoning level: low, medium, high, xhigh, max (provider-dependent)\n --environment <id-or-path> Attach to an existing environment (ID or workspace path)\n --new-environment <kind> Create a new environment (worktree)\n --base-branch <branch> Base branch for a new managed worktree\n --machine <id-or-name> Run on a machine (--host is an alias)\n --service-tier <tier> Service tier: fast, default\n --permission-mode <mode> Permission mode: accept-edits, auto, or full\n --section <id> Create the thread in a section\n --visibility <visibility> visible or hidden; a child inherits its parent by default\n --file <path> Host-readable absolute or uploaded file path\n --image <path> Host-readable absolute or uploaded image path\n --origin-kind <kind> Create a fork thread\n --source-thread <id> Source thread for a fork\n --source-seq-end <seq> Last included source event sequence\n\n Execution defaults resolve from explicit flags, live parent execution, and\n remembered project defaults. With no remembered model, bb uses the explicitly\n requested provider or Codex and resolves its provider-reported default model\n on the target machine. The product reasoning and permission defaults are\n medium and auto.\n accept-edits uses workspace sandboxing with user-reviewed escalation. auto uses\n the same workspace sandbox with provider-native automatic review. full is the\n explicit sandbox and approval bypass. Plan mode is separate from permissions.\n When spawning a subagent, pass --permission-mode full unless the user or task explicitly requests restricted access.\n Parenting is opt-in. Inside a thread, pass --parent-self to parent the new thread to the current thread.\n Hidden threads are for plugin/background workers. They remain addressable by\n ID while staying out of sidebar organization and unread/pending favicon\n attention. Thread lists exclude them unless\n --include-hidden is passed; direct-ID operations remain available.\n A new child thread inherits the visibility of its parent, so the subagents of\n a hidden thread stay hidden too. Pass --visibility to override the inherited\n value. A hidden child still reports its turns and blockers to its parent\n thread; only source-derived forks stay silent.\n A machine selector accepts an exact ID or an unambiguous name. It works with\n an unmanaged --environment path, --new-environment worktree, or the personal\n workspace. It cannot be combined with an existing environment ID because that\n environment already selects its machine. Without the flag, local/primary\n machine resolution is unchanged.\n\nForking:\n\n bb thread fork <source-thread-id> [options]\n\n --prompt <prompt> Optional first prompt; omit for an idle fork\n --source-seq-end <seq> Fork at this source event sequence (tip by default)\n --workspace <mode> isolated (default) or reuse\n --title <title> Thread title\n --permission-mode <mode> Inherit source by default; accepts accept-edits, auto, full\n --visibility <visibility> visible (default) or hidden\n --agent-context-seed <text> Persist agent-only context without a first run\n --file <path> Host-readable absolute or uploaded file path\n --image <path> Host-readable absolute or uploaded image path\n\n Forks clone the source provider session on the same machine. Isolated forks\n create a fresh managed worktree (or personal workspace for personal threads);\n reuse attaches the source environment. Omit --prompt to create an idle fork.\n\nListing:\n\n bb thread list List threads\n --project <id> Filter by project\n --parent-thread <id> Filter by parent thread\n --archived Show only archived threads\n --section <id> Filter by section\n --unsectioned Show only threads outside sections\n --include-hidden Include hidden threads\n\n bb thread search <query> Search threads and messages\n bb thread history <id> List prompt history\n\nSections:\n\n bb thread section list\n bb thread section create <name>\n bb thread section rename <id> <name>\n bb thread section delete <id> [--yes]\n\nInspecting:\n\n bb thread show [id] Show thread details and pull request status\n --self Target current thread\n --work-status Include git working-tree status\n --git-diff Include git diff\n --diff-target <type> Diff scope: uncommitted, branch_committed, all, commit\n --diff-sha <sha> Commit SHA (for --diff-target commit)\n --diff-merge-base <branch> Override merge-base branch for diff\n --merge-base-branches List available merge-base branches\n\n Shows pull request status for the attached environment branch when available.\n\n bb thread log [id] Show thread event log\n --self Target current thread\n --format <format> Output format: json, minimal, verbose\n --limit <count> Limit entries\n --after-seq <seq> Paginate after sequence number\n\n bb thread output [id] Get the final output of a thread\n --self Target current thread\n\n bb thread wait <id> Wait for a thread status or event (defaults to --status idle)\n --status <status> Wait for this status\n --event <type> Wait for this event type\n --timeout <seconds> Timeout in seconds (default: 1200 / 20 min)\n --poll-interval <ms> Polling interval in milliseconds\n\nOpening threads and files in the app:\n\n bb thread open <path> Open a file in the current BB thread panel\n bb thread open <thread-id> [path] Open a thread, optionally with a panel file\n --line <number> Line number to focus\n --split <placement> right, down, left, top, or replace\n bb thread pane <action> [thread-id] Maximize, restore, or toggle an open thread pane\n\n Inside a BB thread, BB_THREAD_ID selects the current thread automatically and\n the thread ID argument is omitted for file-only opens. Pass an explicit thread\n ID with --split to open another thread. Outside a BB thread, pass the thread ID\n as the first argument. A thread already open in a pane is focused instead of\n duplicated. Edge placement creates panes through the eighth pane; at eight\n panes, it replaces the focused pane.\n Pane actions broadcast to connected BB app windows and affect the matching\n already-open pane without changing its split tree.\n Paths can be thread-relative workspace paths, or absolute paths inside the\n target thread workspace. Absolute paths under BB_THREAD_STORAGE open as\n thread-storage files for the current thread. Use this for Markdown or HTML\n artifacts you create for the user so they open in the BB IDE.\n\nMessaging:\n\n bb thread tell <id> <message> Send a follow-up message\n --mode <mode> Message mode: steer (default), queue, or auto\n --model <model> Model override for this turn\n --reasoning-level <level> Reasoning level override\n --file <path> Host-readable absolute or uploaded file path\n --image <path> Host-readable absolute or uploaded image path\n\n Tell steers by default, delivering the message immediately into the active\n turn. Use --mode queue for non-urgent follow-ups that can wait until the agent\n is free.\n\n bb thread stop [id] Stop an active or provisioning thread\n bb thread cancel-plan [id] Exit the provider's active Plan mode\n bb thread clear-goal [id] Clear the provider's active Goal\n --self Target current thread\n\nOwnership:\n\n bb thread update [id] Update thread metadata\n --self Target current thread\n --title <title> Set title\n --parent-thread <id> Assign to a parent thread\n --clear-parent-thread Remove parent assignment\n --section <id> Move into a section\n --clear-section Remove section assignment\n --visibility <visibility> Set visible or hidden\n\n bb thread read [id] Mark read\n bb thread unread [id] Mark unread\n bb thread reorder-pinned <id> [--after <id>] [--before <id>]\n\nQueued messages:\n\n bb thread queue list <thread-id>\n bb thread queue create <thread-id> <message>\n bb thread queue update <thread-id> <message-id> <message> [--file <path>] [--image <path>]\n bb thread queue send <thread-id> <message-id> [--mode auto|steer]\n bb thread queue reorder <thread-id> <message-id> [--after <id>] [--before <id>]\n bb thread queue group <thread-id> <boundary-id> --prefix <comma-separated-ids>\n bb thread queue delete <thread-id> <message-id>\n\nPersisted panel tabs:\n\n bb thread tabs show <thread-id>\n bb thread tabs set <thread-id> --expected-revision <n> --tabs-json '<json>'\n\nLifecycle:\n\n bb thread archive [id] Archive a thread (and children/hidden forks)\n --self Archive current thread\n\n bb thread unarchive [id] Unarchive a thread\n --self Unarchive current thread\n\n bb thread delete <id> Delete permanently\n --yes Skip confirmation\n\nRead-only commands require a thread ID or --self where supported.\nMutating thread lifecycle and messaging commands require an explicit ID or --self.", "fileName": "bb-guide-threads.md", "kind": "instruction", "title": "bb Guide — Threads", diff --git a/packages/templates/src/templates/bb-guide-providers.md b/packages/templates/src/templates/bb-guide-providers.md index dd1005dcce..75d1590c54 100644 --- a/packages/templates/src/templates/bb-guide-providers.md +++ b/packages/templates/src/templates/bb-guide-providers.md @@ -18,8 +18,11 @@ Use these before spawning threads if you are unsure which provider or model to u `--host` is an alias for `--machine`. Machine and environment selectors are mutually exclusive because an environment already selects its machine. When no selector is supplied, both commands intentionally inspect the primary machine. -When provider and model are omitted from bb thread spawn, the project's remembered -defaults apply. +When provider and model are omitted from bb thread spawn, the project's +remembered defaults apply. If the project has no remembered choice, bb uses +the explicitly requested provider or Codex, then resolves the model marked +default by that provider on the target machine (falling back to the first +catalog model when none is marked). Provider-native memory can be controlled on the separate Settings → Providers → Codex and Settings → Providers → Claude Code pages. Codex memory controls diff --git a/packages/templates/src/templates/bb-guide-threads.md b/packages/templates/src/templates/bb-guide-threads.md index 610aee64ab..1b033cd807 100644 --- a/packages/templates/src/templates/bb-guide-threads.md +++ b/packages/templates/src/templates/bb-guide-threads.md @@ -35,7 +35,11 @@ Spawning: --source-thread <id> Source thread for a fork --source-seq-end <seq> Last included source event sequence - Execution defaults resolve from explicit flags, live parent execution, project defaults, then the auto product default. + Execution defaults resolve from explicit flags, live parent execution, and + remembered project defaults. With no remembered model, bb uses the explicitly + requested provider or Codex and resolves its provider-reported default model + on the target machine. The product reasoning and permission defaults are + medium and auto. accept-edits uses workspace sandboxing with user-reviewed escalation. auto uses the same workspace sandbox with provider-native automatic review. full is the explicit sandbox and approval bypass. Plan mode is separate from permissions.