refactor(native-chat): name the structured launch option seed

The create-intent resolver narrowed saved options to model/effort with an
inline key literal, inside a file carrying @ts-nocheck — so neither the
key list nor the string narrowing had a typechecked or testable home, and
the repo already expresses this concept as a named shared shape.

Move it to resolveStructuredLaunchSeedOptions beside the persisted
settings it reads, where it is typechecked and unit-tested, and document
why the seed is exactly model and effort: they are the only ids the
picker persists that both providers also accept as strings.

No behavior change. Adds coverage for a non-string persisted effort,
which settings.json can hold and the durable record must not carry.
This commit is contained in:
Merge Sim
2026-09-06 01:46:01 -07:00
parent 52c78c304a
commit eb33ee6bb3
3 changed files with 84 additions and 10 deletions
@@ -13,7 +13,7 @@ import { getLocalProjectWorktreeGitOptions } from '../project-runtime-git-option
import type { AgentSessionAttachParams } from '../native-chat/agent-session-wire/structured-agent-session-attach' import type { AgentSessionAttachParams } from '../native-chat/agent-session-wire/structured-agent-session-attach'
import { getSystemCodexHomePath } from '../codex/codex-home-paths' import { getSystemCodexHomePath } from '../codex/codex-home-paths'
import { resolveTuiAgentLaunchEnv } from '../../shared/tui-agent-launch-defaults' import { resolveTuiAgentLaunchEnv } from '../../shared/tui-agent-launch-defaults'
import { resolveNativeChatSessionOptionDefaults } from '../../shared/native-chat-session-option-defaults' import { resolveStructuredLaunchSeedOptions } from '../../shared/native-chat-session-option-defaults'
import { hasPersistedStructuredAgentSessionStore as hasPersistedStructuredAgentSessionStoreOnDisk } from './structured-agent-session-runtime' import { hasPersistedStructuredAgentSessionStore as hasPersistedStructuredAgentSessionStoreOnDisk } from './structured-agent-session-runtime'
import { getProfileUserDataPath } from '../orca-profiles/profile-storage-paths' import { getProfileUserDataPath } from '../orca-profiles/profile-storage-paths'
import { homedir } from 'node:os' import { homedir } from 'node:os'
@@ -162,17 +162,10 @@ export class OrcaRuntimeWithResolveRecoveredStructuredTuiTranscript extends Orca
} }
const settings = this.requireStore().getSettings() const settings = this.requireStore().getSettings()
const launchEnv = resolveTuiAgentLaunchEnv(input.agent, settings.agentDefaultEnv) const launchEnv = resolveTuiAgentLaunchEnv(input.agent, settings.agentDefaultEnv)
const defaults = resolveNativeChatSessionOptionDefaults( const options = resolveStructuredLaunchSeedOptions(
settings.nativeChatSessionOptions, settings.nativeChatSessionOptions,
input.agent input.agent
) )
const options = defaults
? Object.fromEntries(
['model', 'effort'].flatMap((key) =>
typeof defaults[key] === 'string' ? [[key, defaults[key]]] : []
)
)
: undefined
const location = await this.resolveStructuredAgentSessionLocation(input.worktree) const location = await this.resolveStructuredAgentSessionLocation(input.worktree)
const workspacePath = (await this.resolveRuntimeFileTarget(input.worktree)).worktree.path const workspacePath = (await this.resolveRuntimeFileTarget(input.worktree)).worktree.path
return { return {
@@ -189,7 +182,7 @@ export class OrcaRuntimeWithResolveRecoveredStructuredTuiTranscript extends Orca
variable: input.agent === 'claude' ? 'CLAUDE_CONFIG_DIR' : 'CODEX_HOME', variable: input.agent === 'claude' ? 'CLAUDE_CONFIG_DIR' : 'CODEX_HOME',
path: await resolveAccountHomePath({ workspacePath, launchEnv, location }) path: await resolveAccountHomePath({ workspacePath, launchEnv, location })
}, },
...(options && Object.keys(options).length > 0 ? { options } : {}), ...(options ? { options } : {}),
runtimeKind: 'native' runtimeKind: 'native'
} }
} }
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import { import {
clearNativeChatSessionOptionModel, clearNativeChatSessionOptionModel,
resolveNativeChatSessionOptionDefaults, resolveNativeChatSessionOptionDefaults,
resolveStructuredLaunchSeedOptions,
updateNativeChatSessionOptionDefaults updateNativeChatSessionOptionDefaults
} from './native-chat-session-option-defaults' } from './native-chat-session-option-defaults'
import type { PersistedNativeChatSessionOptions } from './native-chat-session-options' import type { PersistedNativeChatSessionOptions } from './native-chat-session-options'
@@ -74,3 +75,56 @@ describe('resolveNativeChatSessionOptionDefaults', () => {
}) })
}) })
}) })
describe('resolveStructuredLaunchSeedOptions', () => {
const persistedCodex = (
valuesByModel: Record<string, Record<string, string | boolean>>
): PersistedNativeChatSessionOptions =>
({ codex: { model: 'gpt-5.6-sol', valuesByModel } }) as PersistedNativeChatSessionOptions
it('seeds the saved model and effort a structured create must apply', () => {
expect(
resolveStructuredLaunchSeedOptions(
persistedCodex({ 'gpt-5.6-sol': { effort: 'medium' } }),
'codex'
)
).toEqual({ model: 'gpt-5.6-sol', effort: 'medium' })
})
it('drops ids the providers only accept mid-session', () => {
// `fastMode` is a boolean and `personality` is settable only mid-session;
// neither belongs in the reservation's Record<string, string>.
expect(
resolveStructuredLaunchSeedOptions(
persistedCodex({
'gpt-5.6-sol': { effort: 'high', fastMode: true, personality: 'concise' }
}),
'codex'
)
).toEqual({ model: 'gpt-5.6-sol', effort: 'high' })
})
it('drops a seeded id whose persisted value is not a usable string', () => {
// settings.json is user-writable, so a non-string `effort` must not reach a
// record typed Record<string, string> and be emitted as a turn option.
expect(
resolveStructuredLaunchSeedOptions(
persistedCodex({ 'gpt-5.6-sol': { effort: true } }),
'codex'
)
).toEqual({ model: 'gpt-5.6-sol' })
expect(
resolveStructuredLaunchSeedOptions(
persistedCodex({ 'gpt-5.6-sol': { effort: ' ' } }),
'codex'
)
).toEqual({ model: 'gpt-5.6-sol' })
})
it('seeds nothing until a model is picked, so the CLI default survives', () => {
expect(resolveStructuredLaunchSeedOptions(undefined, 'codex')).toBeUndefined()
expect(
resolveStructuredLaunchSeedOptions({ codex: { valuesByModel: {} } }, 'codex')
).toBeUndefined()
})
})
@@ -28,6 +28,33 @@ export function resolveNativeChatSessionOptionDefaults(
return values return values
} }
/** Why only these two: they are the only ids the picker persists into
* `nativeChatSessionOptions` that both structured providers also accept as
* strings. Claude's `fastMode` is a boolean the durable `Record<string, string>`
* record cannot carry, and the providers' remaining keys are settable only
* mid-session, never seeded at launch. */
const STRUCTURED_LAUNCH_SEED_OPTION_IDS = ['model', 'effort'] as const
/** The saved selection a structured create seeds into its reservation, narrowed
* to the wire-safe string subset the durable record and both providers accept. */
export function resolveStructuredLaunchSeedOptions(
persisted: PersistedNativeChatSessionOptions | null | undefined,
agent: AgentType
): Record<string, string> | undefined {
const defaults = resolveNativeChatSessionOptionDefaults(persisted, agent)
if (!defaults) {
return undefined
}
const seeded: Record<string, string> = {}
for (const id of STRUCTURED_LAUNCH_SEED_OPTION_IDS) {
const value = defaults[id]
if (typeof value === 'string' && value.trim()) {
seeded[id] = value
}
}
return Object.keys(seeded).length > 0 ? seeded : undefined
}
/** Why: an authoritative probe proved this id gone, and a stale `model` is emitted /** Why: an authoritative probe proved this id gone, and a stale `model` is emitted
* verbatim as a launch flag — grok exits fatally on an unknown one. Dropping only * verbatim as a launch flag — grok exits fatally on an unknown one. Dropping only
* `model` keeps the per-model option values for a later reselect. */ * `model` keeps the per-model option values for a later reselect. */