mirror of
https://github.com/stablyai/orca.git
synced 2026-11-05 11:37:25 +00:00
* Use one model catalog per execution scope * Accept a missing session record in host scope * Merge duplicate launch-resolution import * Fix catalog typecheck errors * Align catalog tests with the one-list design * Type the integration fixture's send without a cast * Give the discovery test complete runtime dependencies * Fix model catalog completion and scoped configured display * Preserve model catalog authority across released clients * Fix mobile first-frame catalog fixture narrowing * Keep startup catalog persistence with one completion owner * fix: preserve chat selections across CLI model updates A newer CLI can omit a model that an existing chat still runs. Keep that chat's selected model and effort instead of replacing them from the available list. Discover one model list per host, account, launch command and folder, refreshing after two minutes. Keep current values and the running model's exact effort choices with its chat. Show only the known effort when Claude supplies no matching menu. Preserve older paired-client delivery and configured launch arguments. Make Codex use independent discovery and retain the established ACP and Pi paths. Tracked in STA-10947. * fix: keep the Pi options read within its file limit after main's growth Tracked in STA-10947. * fix: import the option snapshot's shared module once Tracked in STA-10947. * fix: keep Grok and Pi model menus and fast controls across restarts Restore the established ACP and Pi catalog route: newer listed models and qualified startup defaults stay visible, and genuine retained effort menus survive later reads. Keep exact-child Fast and Speed controls. Update the phone option contracts and the remaining stale expectations to the selection rules. Tracked in STA-10947. * fix: share each workspace's model list and keep live Grok and Pi menus New chats read their workspace's own model list through the one shared request for that workspace, so a second chat in the same folder starts on the model and effort the agent actually runs there, and a slow first answer can no longer overwrite a newer list. A running Grok, OpenCode or Pi chat keeps its live model menu when an older shared list arrives later. Update the tests that still expected the replaced behavior. Tracked in STA-10947. * fix: keep separate agent and chat model lists for Grok, OpenCode and Pi A brand-new Claude chat now offers the selected model's effort levels before the first message, as it did before. Grok, OpenCode, omp and Pi again keep the list read from the installed agent apart from the list a running chat reports, and each newer list replaces only its own source. So a chat shows the model the agent is actually running with that model's own effort menu, and an effort change can no longer act on an older model's menu. Saved model lists on WSL are readable again without starting the agent. Tracked in STA-10947. * fix: keep desktop catalog storage out of the phone's type graph and show Grok's late model answer The phone app's type check no longer pulls in desktop-only catalog storage. The model-list code no longer reaches the saved-catalog file through a type import. When a Grok model change times out and Grok later confirms the model, the chat shows the model Grok actually runs. Tracked in STA-10947. * Remove unrelated merge formatter and package manager churn * Clarify independent catalog ownership and refresh contracts * Declare picker demand callback as an effect dependency * Complete mobile catalog surface and catalog merge types * Complete catalog contract fixtures for current host types * fix: preserve catalog source contracts and account fallback * test: inject independent Codex discovery transport * test: remove accidental client fixture assertion * test: qualify the saved account fallback read * fix(chat): preserve catalog evidence and current child controls * Fix catalog CI types and mobile option readiness * Reconcile session controls after inventory changes * Fix catalog import and deferred test resolver types * Keep model capability observations separate from picker views * Use installed model choices for chat options * Fix model catalog test fixture contracts * Restore model option compatibility and stable settlement * Return a catalog reply in provisional launch fixture * Compose unreported live model defaults in the host * Preserve explicit replacement model on recovery send * fix(chat): use stable scoped catalogs on local desktop * test(chat): preserve generic catalog runner type * fix: scope catalogs to configured environment and keep saved reads passive * fix: satisfy catalog regression and controller CI checks
185 lines
7.2 KiB
TypeScript
185 lines
7.2 KiB
TypeScript
import { resolveLoginShellEnvironment } from '../startup/login-shell-environment'
|
|
import { refreshAtRestCommandContext } from '../native-chat/agent-session-wire/at-rest-command-context'
|
|
import {
|
|
mergePersistedWindowsPathAsync,
|
|
readPersistedWindowsPathSegmentsAsync
|
|
} from '../pty/windows-environment-path'
|
|
import type { GlobalSettings } from '../../shared/global-settings-types'
|
|
import {
|
|
nativeChatShellEnvironmentPolicy,
|
|
type NativeChatShellEnvironmentPolicy
|
|
} from '../../shared/native-chat-shell-environment'
|
|
|
|
// Why: with the whole shell off, the child must still find its CLI and keep locale and agent socket.
|
|
const BASELINE_SHELL_VARIABLES = ['PATH', 'LANG', 'LC_ALL', 'LC_CTYPE', 'SSH_AUTH_SOCK']
|
|
|
|
function definedEnv(env: NodeJS.ProcessEnv): Record<string, string> {
|
|
const defined: Record<string, string> = {}
|
|
for (const [key, value] of Object.entries(env)) {
|
|
if (value !== undefined) {
|
|
defined[key] = value
|
|
}
|
|
}
|
|
return defined
|
|
}
|
|
|
|
/** Overlays shell variables on Orca's env; win32 names are case-insensitive, so drop the base spelling. */
|
|
function overlayShellVariables(
|
|
base: Record<string, string>,
|
|
shellVariables: Record<string, string>,
|
|
platform: NodeJS.Platform
|
|
): Record<string, string> {
|
|
const merged = { ...base }
|
|
if (platform === 'win32') {
|
|
const overlaid = new Set(Object.keys(shellVariables).map((key) => key.toUpperCase()))
|
|
for (const key of Object.keys(merged)) {
|
|
if (overlaid.has(key.toUpperCase())) {
|
|
delete merged[key]
|
|
}
|
|
}
|
|
}
|
|
return { ...merged, ...shellVariables }
|
|
}
|
|
|
|
function pickShellVariables(
|
|
shellEnv: NodeJS.ProcessEnv,
|
|
names: readonly string[],
|
|
platform: NodeJS.Platform
|
|
): Record<string, string> {
|
|
const normalize = (key: string): string => (platform === 'win32' ? key.toUpperCase() : key)
|
|
const allowed = new Set([...BASELINE_SHELL_VARIABLES, ...names].map(normalize))
|
|
const picked: Record<string, string> = {}
|
|
for (const [key, value] of Object.entries(definedEnv(shellEnv))) {
|
|
if (allowed.has(normalize(key))) {
|
|
picked[key] = value
|
|
}
|
|
}
|
|
return picked
|
|
}
|
|
|
|
/**
|
|
* The env every structured chat child starts from, before its provider pins an account.
|
|
* Inheriting everything is the login-shell snapshot as-is; otherwise Orca's own env plus
|
|
* the baseline and listed shell variables.
|
|
*/
|
|
export function structuredAgentBaseEnvironment(input: {
|
|
shellEnv: NodeJS.ProcessEnv
|
|
policy: NativeChatShellEnvironmentPolicy
|
|
processEnv?: NodeJS.ProcessEnv
|
|
platform?: NodeJS.Platform
|
|
}): Record<string, string> {
|
|
if (input.policy.inheritAll) {
|
|
return definedEnv(input.shellEnv)
|
|
}
|
|
const platform = input.platform ?? process.platform
|
|
return overlayShellVariables(
|
|
definedEnv(input.processEnv ?? process.env),
|
|
pickShellVariables(input.shellEnv, input.policy.names, platform),
|
|
platform
|
|
)
|
|
}
|
|
|
|
// Why: long enough that a burst of starts shares one capture, short enough that a CLI
|
|
// installed on the host reaches new chats without a restart.
|
|
const SHELL_ENVIRONMENT_TTL_MS = 10_000
|
|
|
|
/**
|
|
* This host's shell env, from the shared login-shell cache: the last result at once, rerun in the
|
|
* background past the TTL. Windows keeps a loaded PowerShell profile for good (only a failed load
|
|
* is retried) and lays the saved PATH, where installers add themselves, over it on every read.
|
|
*/
|
|
async function hostShellEnvironment(cachedOnly = false): Promise<NodeJS.ProcessEnv> {
|
|
const windows = process.platform === 'win32'
|
|
const env = await resolveLoginShellEnvironment({
|
|
cachedOnly,
|
|
refreshAfterMs: SHELL_ENVIRONMENT_TTL_MS,
|
|
keepCaptured: windows
|
|
})
|
|
if (!windows || cachedOnly) {
|
|
return env
|
|
}
|
|
const withSavedPath = { ...env }
|
|
await mergePersistedWindowsPathAsync(withSavedPath)
|
|
return withSavedPath
|
|
}
|
|
|
|
/** For a lookup that started at `askedAt` and found no program: waits for a shell run that started
|
|
* no earlier (on Windows, a saved PATH reread), so an agent installed before the lookup counts.
|
|
* The run's own shell timeout bounds the wait. */
|
|
export async function refreshHostAgentBaseEnvironmentAfterMiss(askedAt: number): Promise<void> {
|
|
await (
|
|
process.platform === 'win32'
|
|
? readPersistedWindowsPathSegmentsAsync({ forceRefresh: true, startedAfter: askedAt })
|
|
: resolveLoginShellEnvironment({ startedAfter: askedAt })
|
|
).catch(() => {})
|
|
}
|
|
|
|
/** Captures this host's shell env again now, so an explicit Refresh never reads a profile kept from
|
|
* before it (Windows keeps a loaded one for good). */
|
|
export async function recaptureHostShellEnvironment(): Promise<void> {
|
|
await refreshAtRestCommandContext(captureHostShellEnvironment)
|
|
}
|
|
|
|
export async function captureHostShellEnvironment(): Promise<void> {
|
|
const startedAfter = Date.now()
|
|
await Promise.all([
|
|
resolveLoginShellEnvironment({ startedAfter }),
|
|
readPersistedWindowsPathSegmentsAsync({ forceRefresh: true, startedAfter })
|
|
]).catch(() => {})
|
|
}
|
|
|
|
/** The base env every local agent launch on this host starts from, read from current settings. */
|
|
export async function resolveHostAgentBaseEnvironment(
|
|
settings: Pick<
|
|
GlobalSettings,
|
|
'nativeChatInheritShellEnvironment' | 'nativeChatShellEnvironmentVariables'
|
|
>
|
|
): Promise<Record<string, string>> {
|
|
return structuredAgentBaseEnvironment({
|
|
shellEnv: await hostShellEnvironment(),
|
|
policy: nativeChatShellEnvironmentPolicy(settings)
|
|
})
|
|
}
|
|
|
|
export type StructuredAgentEnvironmentSources = {
|
|
/** Replaces the host's shell env, read once when the resolvers are made; never reread. */
|
|
resolveEnvironment?: () => Promise<NodeJS.ProcessEnv>
|
|
resolveShellEnvironmentPolicy?: () => NativeChatShellEnvironmentPolicy
|
|
/** The user's per-agent environment overlay from settings. */
|
|
resolveAgentLaunchEnv?: (agent: string) => Record<string, string>
|
|
}
|
|
|
|
/** Every agent's child env over the host's shell env. Policy and overlays are re-read per
|
|
* acquisition, so a settings change reaches the next chat without a restart. */
|
|
export function createStructuredAgentEnvironmentResolvers(
|
|
sources: StructuredAgentEnvironmentSources
|
|
): {
|
|
/** The shared base every agent's child env starts from, before its own overlay. */
|
|
resolveBaseEnvironment: (cachedOnly?: boolean) => Promise<Record<string, string>>
|
|
/** The base with the user's overlay for `agent` laid over it. */
|
|
resolveAgentEnvironment: (agent: string) => Promise<Record<string, string>>
|
|
refreshAfterMissingProgram: (askedAt: number) => Promise<void>
|
|
} {
|
|
// Why: start as soon as the runtime exists so the first chat rarely waits for the profile.
|
|
const injected = sources.resolveEnvironment?.()
|
|
const shellEnvironment = injected ? () => injected : hostShellEnvironment
|
|
if (!injected) {
|
|
void hostShellEnvironment().catch(() => {})
|
|
}
|
|
const resolveBase = async (cachedOnly = false): Promise<Record<string, string>> =>
|
|
structuredAgentBaseEnvironment({
|
|
shellEnv: await shellEnvironment(cachedOnly),
|
|
policy: sources.resolveShellEnvironmentPolicy?.() ?? nativeChatShellEnvironmentPolicy(null)
|
|
})
|
|
return {
|
|
refreshAfterMissingProgram: injected
|
|
? async () => {}
|
|
: refreshHostAgentBaseEnvironmentAfterMiss,
|
|
resolveBaseEnvironment: resolveBase,
|
|
resolveAgentEnvironment: async (agent) => ({
|
|
...(await resolveBase()),
|
|
...sources.resolveAgentLaunchEnv?.(agent)
|
|
})
|
|
}
|
|
}
|