mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 02:27:43 +00:00
* fix: start a new Claude chat on the listed default when its remembered model is gone The remembered new-chat model was launched as-is even after Claude stopped offering it, so every new chat failed its first message. The host create and the desktop new-chat picker now derive the same replacement from the host's saved model list: the listed default, effort moved to the nearest level it offers. Explicit models (--model, held picks) run as given; with no saved list the remembered model is kept. * fix: read the host's model list only for a Claude create that needs it The check read host.deps before ruling out other agents and explicit models, so creates whose host carries no deps (and every non-Claude create) paid for a lookup they never use. * fix: picker derives the fallback only where the host would A held model runs as picked on the host, so the picker no longer derives an effort beside it. A cached list whose default marker was dropped for this workspace keeps the placeholder instead of naming its first row. * fix: the new-chat picker picks the replacement for a gone Claude model The host-side check read the saved list separately from the picker, so the two disagreed and a cold start never fired it. The desktop new-chat picker now picks the replacement it shows, once, through the existing pick path, so the first send runs it and the remembered selection heals. The host is back to main. * fix: decide a gone Claude model's replacement at send, from the list in hand The open-time auto-pick raced the chat's create and the list's arrival, so the first send could still launch the gone model. Now, like the common pattern, the picker shows the replacement for a model the host's loaded list lacks, and the send step applies it through the existing pick call, awaited, right before the message goes out. A failed pick never holds the send back. * fix: one verified list decides a gone model's replacement The picker, the send step and the option setter now all read the list the picker shows, less the row a session adds for its own unlisted model, so what is shown is what applies and an explicit pick from that list stays. A listing that is still running, failed, or absent keeps the model. The send step re-derives from the chat's state when it runs, so a second send behind one that already moved the model applies nothing. * fix: only rows a finished host listing names can replace a gone model A single verified flag outlived the catalog it described: live options built from the built-in fallback list, or a malformed or empty host answer, still authorized a replacement the host would refuse. The state now keeps the ids a finished, well-formed host listing names, and a replacement must be both shown in the picker and named there. * fix(native-chat): the send carries the composer's Claude model and effort Every composer send (desktop and phone) carries the model and effort the picker shows, as optional agentSession.send options, only to a host that advertises agent-session.send-options.v1. The host writes a carried value that differs from the chat's own through the existing pick path before accepting the message, so the turn starts on it and the chat records it. A queued card keeps its options and applies them when it goes out; a launch's create call carries the replacement for its first message. Replaces the separate awaited pick before each send. * chore: drop an unrelated lockfile change * refactor(native-chat): one helper names a send's control fields for client and host * ci: rerun against current main * fix(native-chat): a message runs on the model and effort it was sent with The carried selection is now stored with the message (its submission row, and a queued card's row) and applied through the shared option writer as the delivery loop hands the message over, or before the start it needs. A pick made after the message was accepted no longer changes the model it runs on. Direct sends, queue drains and Send now share that one apply point; the early apply before acceptance and the queue-only apply are gone. The picker and the send read one effective selection: the replacement for a model the host's finished listing lacks shows only for a host that takes a send's selection, the phone validates picks against what it shows, a send carries the default the composer shows, a resumed launch's first message gets the same resolution, and only the listing proves a row (no label heuristic). * docs(native-chat): a submission's options are the host's alone * fix(native-chat): an opening message keeps its model; a pending pick resolves like the picker The opening message a create sends now stores the model and effort the chat was created with, so a pick made before its handover no longer changes what it runs on, and a card kept after a restart keeps them. A pick on its way to the host resolves through the same option view the picker shows before a send carries it, so a gone model it names is replaced too; the send selection reads only the shown snapshot. The option writer builds its own record-and-publish context, so the message path no longer duplicates it, and the client and host share one function for a send's fingerprint fields. * ci: rerun against current main
163 lines
6.7 KiB
TypeScript
163 lines
6.7 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { getAgentSessionOptionCatalog } from './agent-session-option-catalog'
|
|
import type { AgentSessionModelCatalogResult } from './agent-session-wire'
|
|
import {
|
|
applyStructuredAgentSessionModelCatalog,
|
|
applyStructuredAgentSessionOptions,
|
|
createStructuredAgentSessionOptionState,
|
|
settleStructuredAgentSessionBuiltinCatalog,
|
|
structuredAgentSessionOptionSnapshot
|
|
} from './structured-agent-session-options'
|
|
|
|
const SEED = getAgentSessionOptionCatalog('codex')!
|
|
|
|
const HOST_CATALOG: AgentSessionModelCatalogResult = {
|
|
origin: 'live-session',
|
|
models: [
|
|
{
|
|
id: 'gpt-hosted',
|
|
label: 'GPT Hosted',
|
|
isDefault: true,
|
|
defaultEffort: 'high',
|
|
efforts: [
|
|
{ value: 'medium', label: 'Medium' },
|
|
{ value: 'high', label: 'High' }
|
|
]
|
|
}
|
|
],
|
|
fetchedAt: 1_000
|
|
}
|
|
|
|
// HOST_CATALOG omits `listingNamesConfiguredModel`, as an older host's does.
|
|
const LAUNCH = { newLaunch: true }
|
|
|
|
describe('structured option state from the host model catalog', () => {
|
|
it('renders the quiet placeholder before the host answers, never a built-in label', () => {
|
|
const state = createStructuredAgentSessionOptionState('codex', SEED)
|
|
expect(structuredAgentSessionOptionSnapshot(state)).toEqual([
|
|
expect.objectContaining({
|
|
id: 'model',
|
|
valueSource: 'unknown',
|
|
settable: false,
|
|
kind: { type: 'select', choices: [] }
|
|
})
|
|
])
|
|
})
|
|
|
|
it('makes the built-in list pickable, naming nothing, once the host says it has none', () => {
|
|
const state = settleStructuredAgentSessionBuiltinCatalog(
|
|
createStructuredAgentSessionOptionState('codex', SEED)
|
|
)
|
|
expect(state.catalogSource).toBe('builtin')
|
|
const model = structuredAgentSessionOptionSnapshot(state).find((d) => d.id === 'model')!
|
|
expect(model.kind.type === 'select' && model.kind.choices.length).toBeGreaterThan(0)
|
|
expect(model.settable).toBe(true)
|
|
expect(model.kind.type === 'select' ? model.kind.currentValue : null).toBeUndefined()
|
|
})
|
|
|
|
it('upgrades the seed with host models as a provisional, uncommitted default', () => {
|
|
const state = applyStructuredAgentSessionModelCatalog(
|
|
createStructuredAgentSessionOptionState('codex', SEED),
|
|
SEED,
|
|
HOST_CATALOG,
|
|
LAUNCH
|
|
)
|
|
expect(state.catalogSource).toBe('host')
|
|
const snapshot = structuredAgentSessionOptionSnapshot(state)
|
|
const model = snapshot.find((descriptor) => descriptor.id === 'model')!
|
|
expect(model.kind.type === 'select' ? model.kind.currentValue : null).toBe('gpt-hosted')
|
|
// Provisional provenance: a listing default, never a reported value.
|
|
expect(model.valueSource).toBe('default')
|
|
})
|
|
|
|
it('lists host models but names no value for a session it did not launch', () => {
|
|
// A reopened session may run a model picked in it, not the listing's default.
|
|
const state = applyStructuredAgentSessionModelCatalog(
|
|
createStructuredAgentSessionOptionState('codex', SEED),
|
|
SEED,
|
|
HOST_CATALOG,
|
|
{ newLaunch: false }
|
|
)
|
|
const snapshot = structuredAgentSessionOptionSnapshot(state)
|
|
const model = snapshot.find((descriptor) => descriptor.id === 'model')!
|
|
expect(model.kind.type === 'select' && model.kind.choices.map((c) => c.value)).toEqual([
|
|
'gpt-hosted'
|
|
])
|
|
expect(model.kind.type === 'select' ? model.kind.currentValue : null).toBeUndefined()
|
|
const effort = snapshot.find((descriptor) => descriptor.id === 'effort')
|
|
expect(effort?.kind.type === 'select' ? effort.kind.currentValue : undefined).toBeUndefined()
|
|
})
|
|
|
|
it('follows the host on whether its listed default is what a new chat runs', () => {
|
|
const currentModel = (listingNamesConfiguredModel: boolean): unknown => {
|
|
const state = applyStructuredAgentSessionModelCatalog(
|
|
createStructuredAgentSessionOptionState('codex', SEED),
|
|
SEED,
|
|
{ ...HOST_CATALOG, listingNamesConfiguredModel },
|
|
LAUNCH
|
|
)
|
|
const model = structuredAgentSessionOptionSnapshot(state).find((d) => d.id === 'model')!
|
|
return model.kind.type === 'select' ? model.kind.currentValue : null
|
|
}
|
|
expect(currentModel(true)).toBe('gpt-hosted')
|
|
// A workspace whose own config may pick another model: the host says so, over the seed.
|
|
expect(currentModel(false)).toBeUndefined()
|
|
})
|
|
|
|
it('names the default effort the listing states, and none it does not', () => {
|
|
const effortOf = (catalog: AgentSessionModelCatalogResult) => {
|
|
const state = applyStructuredAgentSessionModelCatalog(
|
|
createStructuredAgentSessionOptionState('codex', SEED),
|
|
SEED,
|
|
catalog,
|
|
LAUNCH
|
|
)
|
|
return structuredAgentSessionOptionSnapshot(state).find((d) => d.id === 'effort')!
|
|
}
|
|
const stated = effortOf(HOST_CATALOG)
|
|
expect(stated.kind.type === 'select' ? stated.kind.currentValue : null).toBe('high')
|
|
expect(stated.valueSource).toBe('default')
|
|
const { defaultEffort: _stated, ...unstated } = HOST_CATALOG.models[0]!
|
|
const silent = effortOf({ ...HOST_CATALOG, models: [unstated] })
|
|
expect(silent.kind.type === 'select' ? silent.kind.currentValue : null).toBeUndefined()
|
|
expect(silent.valueSource).toBe('unknown')
|
|
})
|
|
|
|
it('settles on the built-in list on an unknown or empty host answer, and keeps a host list', () => {
|
|
const seeded = createStructuredAgentSessionOptionState('codex', SEED)
|
|
for (const answer of [
|
|
{ origin: 'unknown' as const },
|
|
{ origin: 'probe' as const, models: [], fetchedAt: 1 }
|
|
]) {
|
|
const settled = applyStructuredAgentSessionModelCatalog(seeded, SEED, answer, LAUNCH)
|
|
expect(settled.catalogSource).toBe('builtin')
|
|
expect(settled.catalog).toBe(seeded.catalog)
|
|
const hosted = applyStructuredAgentSessionModelCatalog(seeded, SEED, HOST_CATALOG, LAUNCH)
|
|
// The host list stays; the answer only says it is no longer a finished listing.
|
|
expect(applyStructuredAgentSessionModelCatalog(hosted, SEED, answer, LAUNCH)).toEqual({
|
|
...hosted,
|
|
verifiedModelIds: undefined
|
|
})
|
|
}
|
|
})
|
|
|
|
it('never downgrades a live catalog to a host one', () => {
|
|
const live = applyStructuredAgentSessionOptions(
|
|
createStructuredAgentSessionOptionState('codex', SEED),
|
|
SEED,
|
|
{
|
|
models: [{ id: 'gpt-live', label: 'GPT Live', isDefault: true, efforts: [] }],
|
|
current: { model: 'gpt-live', confirmed: ['model'] }
|
|
}
|
|
)
|
|
expect(live.catalogSource).toBe('live')
|
|
expect(applyStructuredAgentSessionModelCatalog(live, SEED, HOST_CATALOG, LAUNCH)).toEqual({
|
|
...live,
|
|
verifiedModelIds: ['gpt-hosted']
|
|
})
|
|
const snapshot = structuredAgentSessionOptionSnapshot(live)
|
|
const model = snapshot.find((descriptor) => descriptor.id === 'model')!
|
|
expect(model.kind.type === 'select' ? model.kind.currentValue : null).toBe('gpt-live')
|
|
})
|
|
})
|