Files
orca/src/main/text-generation/commit-message-agent-environment.ts
T
JinjingandOrca 094d6821ef feat(native-chat): add model and effort pickers for grok (#12780)
* feat(native-chat): add model and effort pickers for grok

Grok had no session-option catalog, so the native chat composer showed no
pills and every launch ran the CLI's own defaults with no way to change them.

Adds a `GROK_SESSION_OPTION_CATALOG` (model via `-m`/`/model`, reasoning
effort via `--reasoning-effort`/`/effort`) and the discovery plumbing behind
it. Grok's selectable ids depend on the signed-in account and on `[model.*]`
config, so the seed carries only `grok-4.5` and a runtime `grok models` probe
supplies the rest as authoritative — a retired id must be droppable, since
launching one is a fatal exit rather than a warning.

Because `grok models` publishes `Default model:` and marks the row
`(default)`, the picker can name the model a fresh session is actually
running: `defaultModelIsCliDefault` plus an untracked record means no `-m`
was ever emitted, so the CLI is on its own default. That default scopes the
effort row but is never written to persisted settings — that field is what
authorizes `-m` on every later launch, and adopting a model the user never
picked would pin today's default forever, fatally so on an account without
it. `grok --help` publishes no default for `--reasoning-effort`, so the
effort value stays unnamed until something sets it.

Known gap: that refusal to persist is also a limit. An option set while on
the CLI default is dispatched and honored in-session, but reaches no later
launch — it persists under the default's id with `model` left unset, and
both `resolveNativeChatSessionOptionDefaults` and
`resolveAgentSessionOptionLaunch` bail without that key. Picking a model
explicitly persists normally. Closing this means teaching both to resolve
options from the default model while still refusing to emit `-m`, which is
the launch-args path and wants its own review.

Known gap: the picker infers "no `-m` was emitted" from its own in-memory
record, so a model reaching argv from outside it — the user's own
`agentDefaultArgs`, or a renderer reload that drops the record while the
flagged PTY lives on — leaves the pill claiming the CLI default while
another model runs. No wrong model is persisted.

Extracts `hasFlag` and `labelFromModelId`, and splits the model-probe spec
out of the commit-message registry so discovery no longer implies an agent
can write commit messages.

Co-authored-by: Orca <help@stably.ai>

* docs(native-chat): note the invariant keeping modelIsCliDefault agent-safe

The flag is computed without checking the catalog, so it reads as unsafe for
the four agents with no CLI default. It is safe only because `persist` bails
unless `modelId` is truthy, which for those agents implies a tracked model.
Widening that guard would silently change persistence for every agent.

Co-authored-by: Orca <help@stably.ai>

* fix: retire persisted models on mount and handle -- terminator

- When a pane mounts after model discovery has already settled, it now checks
  the cache and retires persisted models that are no longer available.
- CLI flag detection now respects the `--` option terminator, treating
  everything after it as positional arguments rather than flags.

* Fix: persist grok session options under probe-confirmed defaults

Options set under the CLI default were silently lost on restart.
Distinguish seed guesses from probe-confirmed defaults by renaming
`modelIsCliDefault` to `modelIsUnverifiedDefault`. Once confirmed,
adopt the default as a persisted flag so options survive restarts.

* fix(native-chat): close the retired-model fatal-launch paths from counsel review

Counsel report C1/C2 (High), C3, P1, C4:
- Untrack a session model an authoritative discovery dropped and gate every
  persist path, so option writes can never re-adopt a retired id (C1).
- Resolve launch defaults through the enrichment cache: a persisted model
  missing from every settled probe no longer becomes a fatal `-m` (C2).
- Serialize retirement and picks on one settings write queue that re-reads
  live state at apply time (C3).
- Stabilize onSwitchToTerminal so the session-option surface is not rebuilt
  every TerminalPane render (P1), and cap the enrichment host map (C4).

Co-authored-by: Orca <help@stably.ai>

* Store agent in enrichment entry and extract token utilities

Refactor enrichment to store the agent field directly instead of
parsing it from a composite key, and extract CLI flag token filtering
into a shared utility. Use a dedicated function for tracked model ID
lookup. Improves code reuse and reduces parsing overhead.

* Rename modelIsUnverifiedDefault to adoptModelAsLaunchDefault

Move the model adoption gate into the core session-options module, where probe confirmation and discovered-model status are known. This ensures adoption decisions are gate-checked before persisting to avoid fatal launch flags, and simplifies the picker surface by moving the logic to where it belongs.

* Keep model probe evidence by agent, not host

Store probed model IDs in agent-keyed cache independent of host cache, so
evidence persists across host eviction. Prevents retired models from being
treated as valid when host cache entries are evicted.

* Store agent in enrichment entries instead of separate proof-evidence map

Model probe evidence is now tied to enrichment entries rather than maintained in a separate per-agent map, eliminating the need for eviction logic that could disconnect proof from entries.

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-07 17:48:27 -07:00

146 lines
5.3 KiB
TypeScript

import type { ClaudeRuntimeAuthPreparation } from '../claude-accounts/runtime-auth-service'
import { applyClaudeEnvPatch } from '../claude-accounts/environment'
import { readShellStartupEnvVar } from '../pty/shell-startup-env'
import { parseWslUncPath } from '../../shared/wsl-paths'
export type CommitMessageAgentEnvironmentResolvers = {
prepareForCodexLaunch?: (target?: CommitMessageAgentRuntimeTarget) => string | null
prepareForClaudeLaunch?: (
target?: CommitMessageAgentRuntimeTarget
) => Promise<ClaudeRuntimeAuthPreparation>
}
export type CommitMessageAgentRuntimeTarget = {
runtime?: 'host' | 'wsl'
wslDistro?: string | null
}
function cloneProcessEnv(): Record<string, string> {
const env: Record<string, string> = {}
for (const [key, value] of Object.entries(process.env)) {
if (value !== undefined) {
env[key] = value
}
}
return env
}
// Why: with system-default real-home routing, the headless Codex commit run
// must use the user's own ~/.codex. If Orca itself was launched from a nested
// Orca terminal it can inherit an Orca-owned CODEX_HOME override; strip only
// that (CODEX_HOME matching the private ORCA_CODEX_HOME marker), preserving a
// user-set CODEX_HOME.
function cloneProcessEnvWithoutOrcaCodexHomeOverride(): Record<string, string> {
const env = cloneProcessEnv()
if (env.ORCA_CODEX_HOME && env.CODEX_HOME === env.ORCA_CODEX_HOME) {
delete env.CODEX_HOME
}
delete env.ORCA_CODEX_HOME
return env
}
function readInheritedOrShellEnvVar(name: string, sourceName?: string): string | undefined {
return (
(sourceName ? process.env[sourceName] : undefined) ??
process.env[name] ??
readShellStartupEnvVar(name, process.env.HOME, process.env.SHELL)
)
}
function prepareShellConfigDirEnv(agentId: string): { ok: true; env?: NodeJS.ProcessEnv } | null {
const configVar =
agentId === 'opencode'
? 'OPENCODE_CONFIG_DIR'
: agentId === 'pi' || agentId === 'omp'
? 'PI_CODING_AGENT_DIR'
: agentId === 'grok'
? 'GROK_HOME'
: null
if (!configVar) {
return null
}
// Why: each kind owns a distinct ORCA_*_SOURCE_* shadow so a headless commit
// run from inside a legacy OMP overlay restores the OMP source dir, never
// the Pi one (and vice versa). PI_CODING_AGENT_DIR is the binary-facing var
// both kinds consume — see src/main/pi/titlebar-extension-service.ts.
const sourceVar =
agentId === 'opencode'
? 'ORCA_OPENCODE_SOURCE_CONFIG_DIR'
: agentId === 'pi'
? 'ORCA_PI_SOURCE_AGENT_DIR'
: agentId === 'omp'
? 'ORCA_OMP_SOURCE_AGENT_DIR'
: undefined
const value = readInheritedOrShellEnvVar(configVar, sourceVar)
if (!value) {
return { ok: true }
}
// Why: GUI-launched Orca may not inherit shell startup exports, but these
// vars point the headless CLI at the user's auth/config root. Nested Orca
// launches inherit PTY overlays, so prefer ORCA_*_SOURCE_* when present.
return { ok: true, env: { ...cloneProcessEnv(), [configVar]: value } }
}
export async function prepareLocalCommitMessageAgentEnv(
agentId: string,
resolvers: CommitMessageAgentEnvironmentResolvers | undefined,
target?: CommitMessageAgentRuntimeTarget
): Promise<{ ok: true; env?: NodeJS.ProcessEnv } | { ok: false; error: string }> {
// Why: a non-null result short-circuits the resolvers below, so any agent added
// to prepareShellConfigDirEnv must not also need a Codex/Claude-style resolver.
const shellConfigEnv = target?.runtime === 'wsl' ? null : prepareShellConfigDirEnv(agentId)
if (shellConfigEnv) {
return shellConfigEnv
}
if (!resolvers) {
return { ok: true }
}
try {
if (agentId === 'codex' && resolvers.prepareForCodexLaunch) {
const codexHomePath = resolvers.prepareForCodexLaunch(target)
const wslCodexHome = codexHomePath ? parseWslUncPath(codexHomePath) : null
if (target?.runtime === 'wsl') {
const codexHomeForTarget = wslCodexHome?.linuxPath ?? null
// Why: the fallback must still strip Orca-owned overrides, or a
// system-default WSL run inherits the managed CODEX_HOME.
return {
ok: true,
env: codexHomeForTarget
? { ...cloneProcessEnvWithoutOrcaCodexHomeOverride(), CODEX_HOME: codexHomeForTarget }
: cloneProcessEnvWithoutOrcaCodexHomeOverride()
}
}
if (codexHomePath && wslCodexHome) {
// Why: this local generation path spawns the host Codex binary. A WSL
// managed home is only valid when the process is routed through wsl.exe.
return { ok: true }
}
return {
ok: true,
env: codexHomePath
? { ...cloneProcessEnv(), CODEX_HOME: codexHomePath }
: cloneProcessEnvWithoutOrcaCodexHomeOverride()
}
}
if (agentId === 'claude' && resolvers.prepareForClaudeLaunch) {
const preparation = await resolvers.prepareForClaudeLaunch(target)
const env = applyClaudeEnvPatch(cloneProcessEnv(), preparation.envPatch, {
stripAuthEnv: preparation.stripAuthEnv
})
return { ok: true, env }
}
} catch (error) {
console.error('[commit-message] Failed to prepare agent environment:', error)
return {
ok: false,
error: 'Failed to prepare the selected agent account for commit message generation.'
}
}
return { ok: true }
}