Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/main/codex/codex-close-turn-end.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ function sessionWithRunningTurn() {
prompts: new CodexPromptRegistry(),
options: new Map(),
reportedOptions: {},
fastModeTierByModel: new Map(),
dispatchEchoes: createCodexDispatchEchoes(),
turnOpenWaits: createCodexTurnOpenWaits(),
translator
Expand Down
5 changes: 3 additions & 2 deletions src/main/codex/codex-configured-launch-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ function configuredText(value: unknown): string | null {

/** The model and effort the user's Codex config selects, or null when Codex can't say. */
export async function readCodexConfiguredLaunchDefaults(
connection: Pick<CodexAppServerConnection, 'request'>
connection: Pick<CodexAppServerConnection, 'request'>,
timeoutMs = CONFIG_READ_TIMEOUT_MS
): Promise<CodexConfiguredLaunchDefaults | null> {
try {
const response = await connection.request(
'config/read',
{},
{ timeoutMs: CONFIG_READ_TIMEOUT_MS }
{ timeoutMs: Math.min(timeoutMs, CONFIG_READ_TIMEOUT_MS) }
)
const config =
typeof response === 'object' && response !== null && 'config' in response
Expand Down
1 change: 0 additions & 1 deletion src/main/codex/codex-requested-close-turn-timing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ describe('requested-close durable turn timing', () => {
prompts: new CodexPromptRegistry(),
options: new Map(),
reportedOptions: {},
fastModeTierByModel: new Map(),
dispatchEchoes: createCodexDispatchEchoes(),
turnOpenWaits: createCodexTurnOpenWaits(),
translator
Expand Down
16 changes: 4 additions & 12 deletions src/main/codex/codex-structured-acquire-catalog.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { CodexAppServerConnection } from './codex-app-server-connection'
import type { CodexOpenedThread } from './codex-structured-thread-open'
import type {
CodexSessionCatalogAccess,
Expand All @@ -20,23 +19,16 @@ export function codexAcquireCatalogAccess(
return agentModelCatalogSessionAccess(deps.modelCatalog, CODEX_STRUCTURED_AGENT, launch.codexHome)
}

/** The one catalog read a fast-mode restore needs, store-first. Null degrades
* exactly as a failed listing always did: the restore proceeds without tiers. */
export async function codexAcquireFastModeCatalog(input: {
connection: Pick<CodexAppServerConnection, 'request'>
/** Use saved catalog knowledge for Fast restore without waiting on discovery. */
export function codexAcquireFastModeCatalog(input: {
catalogAccess: CodexSessionCatalogAccess | undefined
opened: Pick<CodexOpenedThread, 'model' | 'effort'>
restoreNeedsCatalog: boolean
timeoutMs: number | undefined
}): Promise<CodexSessionOptionCatalog | null> {
}): CodexSessionOptionCatalog | null {
if (!input.restoreNeedsCatalog) {
return null
}
const listing = await codexAcquireCatalogListing(
input.connection,
input.catalogAccess,
input.timeoutMs
)
const listing = codexAcquireCatalogListing(input.catalogAccess)
if (!listing) {
return null
}
Expand Down
39 changes: 39 additions & 0 deletions src/main/codex/codex-structured-acquisition-notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { CodexAppServerConnectionHandlers } from './codex-app-server-connection'
import type { CodexAcquisitionWindow } from './codex-structured-acquisition-window'
import type { CodexDispatchEchoes } from './codex-structured-dispatch-echo'
import type { CodexStructuredNotificationRetry } from './codex-structured-notification-retry'
import { CODEX_RECEIPT_TIMED_METHODS } from './codex-structured-session-state'

/** Stamp provider receipt before pre-publication buffering or retry. */
export function codexAcquisitionNotificationHandler(input: {
acquisition: CodexAcquisitionWindow
sessionId: string
dispatchEchoes: CodexDispatchEchoes
notificationRetries: CodexStructuredNotificationRetry
deliver: (
acquisition: CodexAcquisitionWindow,
sessionId: string,
event: () => unknown,
retainedBytes?: number
) => void
now: () => number
}): NonNullable<CodexAppServerConnectionHandlers['onNotification']> {
return (method, params) => {
const observedAt = CODEX_RECEIPT_TIMED_METHODS.has(method) ? input.now() : undefined
const dispatchSequenceAtReceipt =
method === 'turn/started' ? input.dispatchEchoes.latestSequence() : undefined
input.deliver(
input.acquisition,
input.sessionId,
() =>
input.notificationRetries.handle(
input.sessionId,
method,
params,
observedAt,
dispatchSequenceAtReceipt
),
Buffer.byteLength(JSON.stringify(params ?? null), 'utf8')
)
}
}
110 changes: 110 additions & 0 deletions src/main/codex/codex-structured-background-catalog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type { StructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger'
import {
fetchCodexModelCatalogListing,
type CodexModelCatalogListing
} from './codex-structured-model-catalog'
import type { CodexSession } from './codex-structured-session-state'
import { reconcileCodexFastModeOption } from './codex-structured-fast-mode'

type BackgroundCatalogInput = {
session: CodexSession
sessionId: string
sessions: ReadonlyMap<string, CodexSession>
timeoutMs: number | undefined
logger: StructuredAgentSessionLogger | undefined
}

/** Turns read tiers from the store; only a legacy saved tier needs migrating here. */
function applyListing(session: CodexSession, listing: CodexModelCatalogListing): void {
if (!session.options.has('serviceTier')) {
return
}
const model =
session.options.get('model') ??
session.reportedOptions.model ??
listing.models.find((entry) => entry.isDefault)?.id ??
listing.models[0]?.id ??
''
reconcileCodexFastModeOption(session, {
fastModeTierByModel: listing.fastModeTierByModel,
currentFastMode: undefined,
model,
modelFastModeSupport: undefined
})
}

async function refresh(input: BackgroundCatalogInput): Promise<void> {
const { session, sessionId, sessions } = input
const access = session.catalogAccess
const store = access?.store
const fingerprint = access?.fingerprint
const prior = fingerprint ? store?.get(fingerprint) : undefined
const model = session.options.get('model') ?? session.reportedOptions.model
const needsTier =
(session.options.get('fastMode') === 'true' || session.options.has('serviceTier')) &&
(!model || !prior?.fastModeTierByModel[model])
if (
fingerprint &&
store &&
(store.hasActiveFailure(fingerprint) || (prior && !store.isStale(prior) && !needsTier))
) {
return
}
const isCurrent = (): boolean => sessions.get(sessionId) === session && !session.ended
try {
const listing = await fetchCodexModelCatalogListing({
connection: session.connection,
timeoutMs: input.timeoutMs,
deadlineMs: Math.min(input.timeoutMs ?? 30_000, 30_000)
})
if (listing.models.length === 0) {
throw new Error('codex app-server returned no available models')
}
if (!isCurrent()) {
return
}
const latest = fingerprint ? store?.get(fingerprint) : undefined
if (latest && latest !== prior) {
applyListing(session, {
models: latest.models,
fastModeTierByModel: new Map(Object.entries(latest.fastModeTierByModel))
})
return
}
if (fingerprint && store) {
store.recordSuccess(fingerprint, 'codex', {
models: listing.models,
fastModeTierByModel: listing.fastModeTierByModel,
origin: 'live-session'
})
}
applyListing(session, listing)
} catch (error) {
if (!isCurrent()) {
return
}
if (fingerprint && store && store.get(fingerprint) === prior) {
store.recordFailure(fingerprint, error instanceof Error ? error.message : String(error))
}
input.logger?.warn('Codex model catalog refresh failed', {
scope: 'codex-background-catalog',
sessionId,
error
})
}
}

/** Catalog work begins after ownership and never joins a chat action. */
export function startBackgroundCodexCatalogRefresh(input: BackgroundCatalogInput): void {
void refresh(input).catch((error: unknown) => {
try {
input.logger?.warn('Codex model catalog refresh failed', {
scope: 'codex-background-catalog',
sessionId: input.sessionId,
error
})
} catch {
// Reporting must never reject an unawaited task.
}
})
}
27 changes: 27 additions & 0 deletions src/main/codex/codex-structured-catalog-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { AgentModelCatalogEntry } from '../native-chat/agent-model-catalog/agent-model-catalog-store'
import type { CodexSessionCatalogAccess } from './codex-structured-session-state'
import type { CodexModelCatalogListing } from './codex-structured-model-catalog'

export function listingFromEntry(entry: AgentModelCatalogEntry): CodexModelCatalogListing {
return {
models: entry.models.map((model) => ({ ...model })),
fastModeTierByModel: new Map(Object.entries(entry.fastModeTierByModel))
}
}

/** The exact Fast tier the account's stored catalog names for a model; no I/O. */
export function codexKnownFastModeTier(
catalogAccess: CodexSessionCatalogAccess | undefined,
model: string
): string | undefined {
const tiers = catalogAccess?.store.get(catalogAccess.fingerprint)?.fastModeTierByModel
return tiers && Object.hasOwn(tiers, model) ? tiers[model] : undefined
}

/** Acquisition uses known choices only; picker reads discover new choices later. */
export function codexAcquireCatalogListing(
catalogAccess: CodexSessionCatalogAccess | undefined
): CodexModelCatalogListing | null {
const entry = catalogAccess?.store.get(catalogAccess.fingerprint)
return entry ? listingFromEntry(entry) : null
}
Loading
Loading