From a6c450718a64d365cd46c41ca45a96eb37dfdb90 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:21:53 -0700 Subject: [PATCH] fix(native-chat): a remembered effort a saved Pi listing lacks no longer refuses new chats (carries #31576) (#33637) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New native-chat Pi sessions are no longer refused before Pi starts when a remembered effort (e.g. xhigh) isn't listed in Orca's saved `pi --list-models` row: a remembered effort the row doesn't list moves to the nearest listed level (explicit --model/--effort stay strict), and the remembered preference itself is left intact. Pi clamps unsupported levels itself. Fixes #31493 Supersedes #31576 Co-authored-by: 王浩生 <22971846+hualong1009@users.noreply.github.com> --- .../runtime/agent-session-record-options.ts | 30 +++++++ ...gent-session-create-seed-admission.test.ts | 89 +++++++++++++++++++ .../structured-agent-session-create.ts | 13 ++- 3 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 src/main/runtime/rpc/methods/structured-agent-session-create-seed-admission.test.ts diff --git a/src/main/runtime/agent-session-record-options.ts b/src/main/runtime/agent-session-record-options.ts index dc98c38a17a..fbcc973ff46 100644 --- a/src/main/runtime/agent-session-record-options.ts +++ b/src/main/runtime/agent-session-record-options.ts @@ -6,6 +6,7 @@ import type { AgentSessionRecord } from '../../shared/agent-session-record' import type { AgentSessionModelOption } from '../../shared/agent-session-wire' +import { nearestAgentEffort } from './session-tabs/structured-launch-seed-listing' export function replaceAgentSessionRecordOptions( record: AgentSessionRecord, @@ -47,6 +48,35 @@ export function agentSessionOptionAdmitted( ) } +/** Fit a remembered seed to its model's saved row: an effort the row lacks moves to the nearest listed + * level, other unlisted values drop; keys the caller named this launch keep strict admission. */ +export function seedOptionsAdmittedOnSavedListing( + provider: string, + seed: Readonly>, + models: AgentSessionAdmissionModels, + explicit: ReadonlySet +): Record | undefined { + const admitted: Record = {} + for (const [key, value] of Object.entries(seed)) { + if (explicit.has(key) || agentSessionOptionAdmitted(provider, models, seed.model, key, value)) { + admitted[key] = value + continue + } + const row = models?.find((entry) => entry.id === seed.model) + const nearest = + key === 'effort' && row + ? nearestAgentEffort( + value, + row.efforts.map((choice) => choice.value) + ) + : undefined + if (nearest) { + admitted[key] = nearest + } + } + return Object.keys(admitted).length > 0 ? admitted : undefined +} + export function assertAgentSessionOptionsAdmitted( provider: string, models: AgentSessionAdmissionModels, diff --git a/src/main/runtime/rpc/methods/structured-agent-session-create-seed-admission.test.ts b/src/main/runtime/rpc/methods/structured-agent-session-create-seed-admission.test.ts new file mode 100644 index 00000000000..d6f18a99bd4 --- /dev/null +++ b/src/main/runtime/rpc/methods/structured-agent-session-create-seed-admission.test.ts @@ -0,0 +1,89 @@ +import { expect, it, vi } from 'vitest' +import type { AgentSessionAttachParams } from '../../../native-chat/agent-session-wire/structured-agent-session-attach' +import type { StructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-host' +import type { OrcaRuntimeService } from '../../orca-runtime' +import { prepareStructuredAgentSessionCreateForWorktree } from './structured-agent-session-create' + +const listing = (models: unknown[]) => ({ origin: 'probe', models }) + +async function prepareWithSeed( + seed: Readonly> | undefined, + options: Readonly> | undefined, + listingModels: unknown[] | null +): Promise { + const attachParams = { + envelope: { + sessionId: 'pi_session_1', + clientOperationId: 'operation-1', + expectedRuntimeFence: null, + payloadFingerprint: '' + }, + location: { + executionHostId: 'local', + wslDistro: null, + workspaceId: 'workspace-1', + workspaceKind: 'git-worktree' + }, + provider: 'pi', + agent: 'pi', + accountHome: { variable: 'HOME', path: '/home/user' }, + runtimeKind: 'native' as const, + ...(seed ? { options: seed } : {}) + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stub implements only the members prepare reaches. + const host = { + deps: { + modelCatalog: listingModels ? { read: vi.fn(async () => listing(listingModels)) } : undefined + } + } as unknown as StructuredAgentSessionHost + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the stub implements only the members prepare reaches. + const runtime = { + resolveStructuredAgentSessionCreateIntent: vi.fn(async () => attachParams), + resolveStructuredAgentSessionLocalWorkspacePath: vi.fn(async () => '/work/repo') + } as unknown as OrcaRuntimeService + const prepared = await prepareStructuredAgentSessionCreateForWorktree({ + runtime, + ensureHost: async () => host, + envelope: attachParams.envelope, + worktree: 'id:workspace-1', + agent: 'pi', + caller: { callerKey: 'caller-1' }, + ...(options ? { options } : {}) + }) + return prepared.attachParams.options +} + +const piRow = (efforts: readonly string[]) => ({ + id: 'some/model', + label: 'Some Model', + isDefault: false, + efforts: efforts.map((value) => ({ value, label: value })) +}) + +it('moves a remembered effort the saved probe row lacks to its nearest listed level', async () => { + // Pi's live menu offers xhigh where its --list-models probe stops at high. + const options = await prepareWithSeed({ model: 'some/model', effort: 'xhigh' }, undefined, [ + piRow(['off', 'minimal', 'low', 'medium', 'high']) + ]) + expect(options).toEqual({ model: 'some/model', effort: 'high' }) +}) + +it('drops a remembered effort its saved row no longer admits', async () => { + const options = await prepareWithSeed({ model: 'some/model', effort: 'off' }, undefined, [ + piRow([]) + ]) + expect(options).toEqual({ model: 'some/model' }) +}) + +it('keeps an effort this launch named itself, even one its row does not admit', async () => { + // Explicit options replace the saved selection; only their own values stay the caller's picks. + const options = await prepareWithSeed({ model: 'some/model' }, { effort: 'off' }, [piRow([])]) + expect(options).toEqual({ effort: 'off' }) +}) + +it('keeps a remembered effort when no saved rows name its model', async () => { + const options = await prepareWithSeed({ model: 'some/model', effort: 'off' }, undefined, [ + { id: 'other/model', label: 'Other Model', isDefault: false, efforts: [] } + ]) + expect(options).toEqual({ model: 'some/model', effort: 'off' }) +}) diff --git a/src/main/runtime/rpc/methods/structured-agent-session-create.ts b/src/main/runtime/rpc/methods/structured-agent-session-create.ts index adcbe7d45b9..20d91d07b3d 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session-create.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session-create.ts @@ -35,6 +35,7 @@ import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switc import { structuredAgentSessionClosedBeforeCreate } from './structured-agent-session-close-before-create' import { structuredLaunchSeedOnSavedListing } from '../../session-tabs/structured-launch-seed-listing' import type { ChatTabPlacement } from '../../agent-session/agent-session-tab-layout' +import { seedOptionsAdmittedOnSavedListing } from '../../agent-session-record-options' export type PreparedStructuredAgentSessionCreate = { host: StructuredAgentSessionHost @@ -165,13 +166,23 @@ export async function prepareStructuredAgentSessionCreateForWorktree(args: { await catalogFolder() ) : undefined + // Fit remembered values to the saved row instead of refusing the create (#31493). + const admittedSeed = + options && catalogModels && !args.options?.model + ? seedOptionsAdmittedOnSavedListing( + resolved.provider, + options, + catalogModels, + new Set(Object.keys(args.options ?? {})) + ) + : options return { host, ...(catalogModels ? { catalogModels } : {}), ...(hostLaunchDirectory ? { hostLaunchDirectory } : {}), attachParams: { ...resolvedAttach, - ...(options ? { options } : {}), + ...(admittedSeed ? { options: admittedSeed } : {}), ...(args.tabId ? { surfaceTabId: args.tabId } : {}), provider: resolved.provider, agent: resolved.agent,