Files
a32781537f fix(query): bound per-turn latency growth in long REPL sessions (#1949) (#1952)
* fix(query): bound per-turn latency growth in long REPL sessions (#1949)

Addresses the progressive latency regression where consecutive prompts in a
single session grow non-linearly (2nd prompt ~10s, 3rd 10+ min) due to
unbounded message accumulation with no proactive compaction and no per-prompt
turn cap on the main thread.

- Cap the interactive REPL main thread at 50 turns per prompt (DEFAULT_REPL_MAX_TURNS).
  Headless/print mode and the SDK are unchanged (--max-turns flag / SDK callers
  still control it), preserving the SDK API contract.
- Default maxMessagesCompactionThreshold to '200' so message-count compaction
  runs well before the context window fills, instead of 'off'.
- Lower the auto-compact threshold buffer from 13k -> 30k so compaction fires
  earlier with less accumulated history. The effective-context floor buffer is
  kept at 13k and getAutoCompactThreshold() falls back to it for small-context
  models, so the threshold can never go negative (no #635 regression).

Test updates: isolate the hard-cap override test from the new 200-message
default, and correct an outdated constant reference in the autoCompact test.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(query): repair REPL latency guard

* fix(query): cover resume and default guard paths

* fix(query): enforce cap across interactive paths

* docs(compaction): clarify disabled message limits

* fix(query): retain explicit message thresholds

* fix(query): enforce explicit threshold recovery

* fix(query): honor legacy active-message limit

* fix(doctor): report effective message compaction limit

* fix(config): share message threshold validation

* test(doctor): cover disabled message compaction

* fix(compact): preserve latency guard coverage

* test(repl): exercise turn cap defaults

* fix(compact): honor disabled default message guard

* fix(swarm): honor disabled auto compaction

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-15 23:06:53 +08:00

1450 lines
46 KiB
TypeScript

// @ts-nocheck
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { spawnSync } from 'node:child_process'
import {
resolveCodexApiCredentials,
resolveProviderRequest,
isLocalProviderUrl as isProviderLocalUrl,
} from '../src/services/api/providerConfig.js'
import {
getRouteCredentialEnvVars,
getRouteCredentialValue,
resolveActiveRouteIdFromEnv,
resolveRouteIdFromBaseUrl,
} from '../src/integrations/routeMetadata.js'
import {
getLocalOpenAICompatibleProviderLabel,
probeOllamaGenerationReadiness,
} from '../src/utils/providerDiscovery.js'
import {
DEFAULT_GEMINI_MODEL,
resolveOpenAICredentialEnvState,
} from '../src/utils/providerProfile.js'
import {
redactSecretValueForDisplay,
redactSecretSubstringsForDisplay,
type SecretValueSource,
} from '../src/utils/providerSecrets.js'
import { redactUrlForDisplay } from '../src/utils/redaction.js'
import {
MIN_NODE_ENGINE_RANGE,
checkSupportedNodeVersion,
} from '../src/utils/nodeRuntime.js'
import { SandboxManager } from '../src/utils/sandbox/sandbox-adapter.js'
import {
DEFAULT_MAX_ACTIVE_MESSAGES_HARD_CAP,
getMaxActiveMessagesHardCap,
} from '../src/utils/maxActiveMessages.js'
import {
isValidMaxMessagesCompactionThreshold,
normalizeMaxMessagesCompactionThreshold,
} from '../src/utils/config.js'
import {
getAvailableProviders,
getProviderChain,
getProviderMode,
type ProviderMode,
} from '../src/tools/WebSearchTool/providers/index.js'
import { getWebSearchTimeoutMs } from '../src/tools/WebSearchTool/providers/timeout.js'
import { isFirecrawlCloudApiUrl } from '../src/tools/firecrawl/client.js'
import { getAPIProvider } from '../src/utils/model/providers.js'
import { getMainLoopModel } from '../src/utils/model/model.js'
type CheckResult = {
ok: boolean
label: string
detail?: string
}
type MemoryGuardConfigInput = {
autoCompactEnabled: boolean
maxMessagesCompactionThreshold?: string
env?: NodeJS.ProcessEnv
}
type NodeExecutableVersionProbe =
| {
ok: true
version: string
}
| {
ok: false
detail: string
}
type CliOptions = {
json: boolean
outFile: string | null
}
function pass(label: string, detail?: string): CheckResult {
return { ok: true, label, detail }
}
function fail(label: string, detail?: string): CheckResult {
return { ok: false, label, detail }
}
function isTruthy(value: string | undefined): boolean {
if (!value) return false
const normalized = value.trim().toLowerCase()
return normalized !== '' && normalized !== '0' && normalized !== 'false' && normalized !== 'no'
}
function parsePositiveInteger(value: string | undefined): number {
if (!value) return 0
const trimmed = value.trim()
if (!/^[1-9]\d*$/.test(trimmed)) return 0
const parsed = Number.parseInt(trimmed, 10)
return Number.isSafeInteger(parsed) ? parsed : 0
}
function formatActiveHardCapDetail(
hardCap: number,
rawOverride: string | undefined,
): string {
if (rawOverride === undefined) {
return `Active at ${hardCap} messages (default; malformed overrides fall back to ${DEFAULT_MAX_ACTIVE_MESSAGES_HARD_CAP}).`
}
if (parsePositiveInteger(rawOverride) > 0) {
return `Active at ${hardCap} messages.`
}
return `Active at ${hardCap} messages; malformed override fell back to ${DEFAULT_MAX_ACTIVE_MESSAGES_HARD_CAP}.`
}
export function buildMemoryGuardChecks(
input: MemoryGuardConfigInput,
): CheckResult[] {
const env = input.env ?? process.env
const results: CheckResult[] = []
const disableCompact = isTruthy(env.DISABLE_COMPACT)
const disableAutoCompact = isTruthy(env.DISABLE_AUTO_COMPACT)
const autoCompactAvailable =
input.autoCompactEnabled && !disableCompact && !disableAutoCompact
const hardCapOverride = env.OPENCLAUDE_MAX_ACTIVE_MESSAGES_HARD_CAP
const hardCap = getMaxActiveMessagesHardCap(env)
const legacyLimit = parsePositiveInteger(env.OPENCLAUDE_MAX_ACTIVE_MESSAGES)
const configuredLimit =
((input.maxMessagesCompactionThreshold === undefined ||
input.maxMessagesCompactionThreshold === 'off') &&
legacyLimit > 0)
? legacyLimit
: normalizeMaxMessagesCompactionThreshold(
input.maxMessagesCompactionThreshold,
)
const hasExplicitMessageCountGuard =
input.maxMessagesCompactionThreshold !== undefined &&
isValidMaxMessagesCompactionThreshold(
input.maxMessagesCompactionThreshold,
) &&
input.maxMessagesCompactionThreshold !== 'off'
const hasLegacyMessageCountGuard =
(input.maxMessagesCompactionThreshold === undefined ||
input.maxMessagesCompactionThreshold === 'off') &&
legacyLimit > 0
const memoryBudget = parsePositiveInteger(env.OPENCLAUDE_MAX_MEMORY_MB) || 1536
const hasIndependentMessageCountGuard =
configuredLimit !== 'off' &&
(hasExplicitMessageCountGuard || hasLegacyMessageCountGuard)
const autoCompactDisabledReason =
[
input.autoCompactEnabled ? undefined : 'settings disabled',
disableCompact ? 'DISABLE_COMPACT is set' : undefined,
disableAutoCompact ? 'DISABLE_AUTO_COMPACT is set' : undefined,
].filter(Boolean).join('; ') || 'Disabled by configuration.'
results.push(
autoCompactAvailable
? pass(
'Auto-compact guard',
`Enabled; message-count threshold ${configuredLimit}; hard cap ${hardCap === 0 ? 'disabled' : hardCap}.`,
)
: fail(
'Auto-compact guard',
autoCompactDisabledReason +
(hasIndependentMessageCountGuard
? `; message-count threshold ${configuredLimit} remains active.`
: ''),
),
)
results.push(
hardCap === 0
? fail(
'Active-message hard cap',
'Disabled by OPENCLAUDE_MAX_ACTIVE_MESSAGES_HARD_CAP=0; long sessions can grow without the active-message safety cap.',
)
: pass(
'Active-message hard cap',
formatActiveHardCapDetail(hardCap, hardCapOverride),
),
)
results.push(
pass(
'Memory pressure guard',
`Per-session budget ${memoryBudget}MB; elevated/critical compaction thresholds are derived from this budget at runtime.`,
),
)
return results
}
const WEB_SEARCH_API_PROVIDER_NAMES = new Set([
'firecrawl',
'tavily',
'exa',
'you',
'jina',
'brave',
'bing',
'mojeek',
'linkup',
])
const WEB_SEARCH_RELIABLE_BACKEND_ENV_HINT =
'FIRECRAWL_API_KEY, TAVILY_API_KEY, EXA_API_KEY, YOU_API_KEY, JINA_API_KEY, BRAVE_API_KEY, BING_API_KEY, MOJEEK_API_KEY, or LINKUP_API_KEY'
const WEB_SEARCH_PROVIDER_ENV_VARS: Record<
Exclude<ProviderMode, 'auto' | 'native'>,
string[]
> = {
custom: ['WEB_SEARCH_API', 'WEB_PROVIDER', 'WEB_URL_TEMPLATE'],
firecrawl: ['FIRECRAWL_API_KEY', 'FIRECRAWL_API_URL'],
ddg: [],
tavily: ['TAVILY_API_KEY'],
exa: ['EXA_API_KEY'],
you: ['YOU_API_KEY'],
jina: ['JINA_API_KEY'],
brave: ['BRAVE_API_KEY'],
bing: ['BING_API_KEY'],
mojeek: ['MOJEEK_API_KEY'],
linkup: ['LINKUP_API_KEY'],
}
const WEB_SEARCH_CUSTOM_PRESET_REQUIRED_ENV_VARS: Record<string, string[]> = {
google: ['WEB_KEY', 'GOOGLE_CSE_ID'],
brave: ['WEB_KEY'],
serpapi: ['WEB_KEY'],
searxng: [],
}
function formatEnvVarList(envVars: string[]): string {
if (envVars.length <= 1) return envVars[0] ?? ''
if (envVars.length === 2) return `${envVars[0]} or ${envVars[1]}`
return `${envVars.slice(0, -1).join(', ')}, or ${envVars[envVars.length - 1]}`
}
function formatAndList(values: string[]): string {
if (values.length <= 1) return values[0] ?? ''
if (values.length === 2) return `${values[0]} and ${values[1]}`
return `${values.slice(0, -1).join(', ')}, and ${values[values.length - 1]}`
}
function formatDuckDuckGoReliabilityDetail(providerMode: string): string {
return `${providerMode}; DuckDuckGo selected. DuckDuckGo scraping can be rate-limited from datacenter/VPN/repeated-request networks. Configure ${WEB_SEARCH_RELIABLE_BACKEND_ENV_HINT} for reliable search.`
}
function vertexModelSupportsNativeWebSearch(model: string): boolean {
return (
model.includes('claude-opus-4') ||
model.includes('claude-sonnet-4') ||
model.includes('claude-haiku-4')
)
}
function isCodexResponsesWebSearchEnabledForDoctor(): boolean {
const request = resolveProviderRequest({
model: getMainLoopModel(),
baseUrl: process.env.OPENAI_BASE_URL,
})
return request.transport === 'codex_responses'
}
function buildNativeWebSearchCheck(): CheckResult {
const provider = getAPIProvider()
if (provider === 'openai' && isCodexResponsesWebSearchEnabledForDoctor()) {
return pass(
'Web search backend',
'WEB_SEARCH_PROVIDER=native selected; Codex responses provider supports web search.',
)
}
if (provider === 'firstParty' || provider === 'foundry') {
return pass(
'Web search backend',
`WEB_SEARCH_PROVIDER=native selected; ${provider} provider supports native web search.`,
)
}
if (provider === 'vertex') {
const model = getMainLoopModel()
if (vertexModelSupportsNativeWebSearch(model)) {
return pass(
'Web search backend',
`WEB_SEARCH_PROVIDER=native selected; vertex provider supports native web search for ${safeDisplayValue(model, 'the active model')}.`,
)
}
return fail(
'Web search backend',
`WEB_SEARCH_PROVIDER=native selected, but vertex model ${safeDisplayValue(model, 'the active model')} does not support native web search. Use a Claude 4 Vertex model or configure ${WEB_SEARCH_RELIABLE_BACKEND_ENV_HINT}.`,
)
}
return fail(
'Web search backend',
`WEB_SEARCH_PROVIDER=native selected, but ${provider} provider does not support native web search. Configure ${WEB_SEARCH_RELIABLE_BACKEND_ENV_HINT}, or switch to an Anthropic, Vertex, Foundry, or Codex responses provider.`,
)
}
function buildAutoNativeWebSearchCheck(): CheckResult | undefined {
const provider = getAPIProvider()
if (provider === 'openai' && isCodexResponsesWebSearchEnabledForDoctor()) {
return pass(
'Web search backend',
'WEB_SEARCH_PROVIDER=auto; Codex responses web search will be used before adapter providers.',
)
}
if (provider === 'firstParty' || provider === 'foundry') {
return pass(
'Web search backend',
`WEB_SEARCH_PROVIDER=auto; ${provider} native web search will be used before adapter providers.`,
)
}
if (provider === 'vertex') {
const model = getMainLoopModel()
if (vertexModelSupportsNativeWebSearch(model)) {
return pass(
'Web search backend',
`WEB_SEARCH_PROVIDER=auto; vertex native web search will be used before adapter providers for ${safeDisplayValue(model, 'the active model')}.`,
)
}
return fail(
'Web search backend',
`WEB_SEARCH_PROVIDER=auto selected, but vertex model ${safeDisplayValue(model, 'the active model')} does not support native web search and runtime will not use adapter providers in auto mode. Use a Claude 4 Vertex model or set an explicit WEB_SEARCH_PROVIDER adapter mode with ${WEB_SEARCH_RELIABLE_BACKEND_ENV_HINT}.`,
)
}
return undefined
}
function getConfiguredWebSearchApiProviderNames(): string[] {
return getAvailableProviders()
.map(provider => provider.name)
.filter(providerName => WEB_SEARCH_API_PROVIDER_NAMES.has(providerName))
.filter(isWebSearchApiProviderConfiguredForDoctor)
}
function appendConfiguredWebSearchApiProviderDetail(result: CheckResult): CheckResult {
const configuredProviders = getConfiguredWebSearchApiProviderNames()
if (configuredProviders.length === 0) return result
const providerDetail = `Configured API-backed providers: ${configuredProviders.join(', ')}.`
return {
...result,
detail: result.detail ? `${result.detail} ${providerDetail}` : providerDetail,
}
}
function hasFirecrawlRunnableConfig(): boolean {
return Boolean(process.env.FIRECRAWL_API_KEY) ||
Boolean(process.env.FIRECRAWL_API_URL && !isFirecrawlCloudApiUrl(process.env.FIRECRAWL_API_URL))
}
function buildFirecrawlWebSearchCheck(): CheckResult {
const apiKey = process.env.FIRECRAWL_API_KEY
const apiUrl = process.env.FIRECRAWL_API_URL
if (!apiKey && isFirecrawlCloudApiUrl(apiUrl)) {
return fail(
'Web search backend',
'WEB_SEARCH_PROVIDER=firecrawl but FIRECRAWL_API_KEY is missing for the Firecrawl cloud API.',
)
}
if (apiKey) {
return pass(
'Web search backend',
'WEB_SEARCH_PROVIDER=firecrawl; FIRECRAWL_API_KEY configured.',
)
}
if (apiUrl) {
return pass(
'Web search backend',
'WEB_SEARCH_PROVIDER=firecrawl; FIRECRAWL_API_URL configured.',
)
}
return fail(
'Web search backend',
'WEB_SEARCH_PROVIDER=firecrawl but FIRECRAWL_API_KEY is missing.',
)
}
function getAutoFirecrawlMissingCredentialDetail(): string | undefined {
const firecrawlSelectedByAutoChain = getAvailableProviders()
.some(provider => provider.name === 'firecrawl')
if (!firecrawlSelectedByAutoChain || hasFirecrawlRunnableConfig()) return undefined
return 'FIRECRAWL_API_URL points to the Firecrawl cloud API but FIRECRAWL_API_KEY is missing; runtime will try firecrawl first and then fall through to the next provider in auto mode.'
}
function appendAutoFirecrawlMissingCredentialDetail(result: CheckResult): CheckResult {
const firecrawlDetail = getAutoFirecrawlMissingCredentialDetail()
if (!firecrawlDetail) return result
return {
...result,
detail: result.detail ? `${result.detail} ${firecrawlDetail}` : firecrawlDetail,
}
}
function isWebSearchApiProviderConfiguredForDoctor(providerName: string): boolean {
return providerName !== 'firecrawl' || hasFirecrawlRunnableConfig()
}
function buildCustomWebSearchCheck(providerConfigured: boolean): CheckResult {
const providerPreset = process.env.WEB_PROVIDER
const trimmedProviderPreset = providerPreset?.trim()
const customUrlEnv = process.env.WEB_URL_TEMPLATE
? 'WEB_URL_TEMPLATE'
: process.env.WEB_SEARCH_API
? 'WEB_SEARCH_API'
: undefined
if (!providerConfigured) {
return fail(
'Web search backend',
'WEB_SEARCH_PROVIDER=custom but WEB_SEARCH_API, WEB_PROVIDER, or WEB_URL_TEMPLATE is missing.',
)
}
if (!providerPreset) {
return pass(
'Web search backend',
`WEB_SEARCH_PROVIDER=custom; ${customUrlEnv ?? formatEnvVarList(WEB_SEARCH_PROVIDER_ENV_VARS.custom)} configured.`,
)
}
const requiredEnvVars = WEB_SEARCH_CUSTOM_PRESET_REQUIRED_ENV_VARS[providerPreset]
if (!requiredEnvVars) {
if (!customUrlEnv) {
if (
trimmedProviderPreset &&
trimmedProviderPreset !== providerPreset &&
WEB_SEARCH_CUSTOM_PRESET_REQUIRED_ENV_VARS[trimmedProviderPreset]
) {
return fail(
'Web search backend',
`WEB_SEARCH_PROVIDER=custom with WEB_PROVIDER=${trimmedProviderPreset} but the raw WEB_PROVIDER value has surrounding whitespace and does not match a runtime custom preset. Remove the whitespace or configure WEB_SEARCH_API or WEB_URL_TEMPLATE.`,
)
}
return fail(
'Web search backend',
`WEB_SEARCH_PROVIDER=custom with WEB_PROVIDER=${providerPreset} but WEB_SEARCH_API or WEB_URL_TEMPLATE is missing for an unknown custom preset.`,
)
}
return pass(
'Web search backend',
`WEB_SEARCH_PROVIDER=custom; WEB_PROVIDER and ${customUrlEnv} configured.`,
)
}
if (providerPreset === 'searxng' && !customUrlEnv) {
return fail(
'Web search backend',
'WEB_SEARCH_PROVIDER=custom with WEB_PROVIDER=searxng but WEB_SEARCH_API or WEB_URL_TEMPLATE is missing for the SearXNG endpoint.',
)
}
const missingEnvVars = requiredEnvVars.filter(envVar => !process.env[envVar])
if (missingEnvVars.length > 0) {
return fail(
'Web search backend',
`WEB_SEARCH_PROVIDER=custom with WEB_PROVIDER=${providerPreset} but ${formatAndList(missingEnvVars)} ${missingEnvVars.length === 1 ? 'is' : 'are'} missing.`,
)
}
return pass(
'Web search backend',
`WEB_SEARCH_PROVIDER=custom; ${formatAndList(['WEB_PROVIDER', ...requiredEnvVars])} configured.`,
)
}
function formatWebSearchTimeoutSeconds(): string {
return String(getWebSearchTimeoutMs() / 1000)
}
function appendWebSearchTimeoutDetail(result: CheckResult): CheckResult {
const timeoutDetail = `Built-in provider timeout: ${formatWebSearchTimeoutSeconds()}s.`
return {
...result,
detail: result.detail ? `${result.detail} ${timeoutDetail}` : timeoutDetail,
}
}
function appendWebSearchTimeoutDetails(results: CheckResult[]): CheckResult[] {
return results.map(appendWebSearchTimeoutDetail)
}
function buildWebSearchEnvChecks(): CheckResult[] {
const mode = getProviderMode()
if (mode === 'native') {
return [appendConfiguredWebSearchApiProviderDetail(buildNativeWebSearchCheck())]
}
if (mode === 'auto') {
const nativeCheck = buildAutoNativeWebSearchCheck()
if (nativeCheck) {
return [appendConfiguredWebSearchApiProviderDetail(nativeCheck)]
}
const configuredProviders = getConfiguredWebSearchApiProviderNames()
if (configuredProviders.length > 0) {
return appendWebSearchTimeoutDetails([
appendAutoFirecrawlMissingCredentialDetail(
pass(
'Web search backend',
`WEB_SEARCH_PROVIDER=auto; configured providers: ${configuredProviders.join(', ')}; fallback includes duckduckgo.`,
),
),
])
}
return appendWebSearchTimeoutDetails([
appendAutoFirecrawlMissingCredentialDetail(
pass(
'Web search backend',
`WEB_SEARCH_PROVIDER=auto; only DuckDuckGo fallback is available. DuckDuckGo scraping can be rate-limited from datacenter/VPN/repeated-request networks. Configure ${WEB_SEARCH_RELIABLE_BACKEND_ENV_HINT} for reliable search.`,
),
),
])
}
if (mode === 'ddg') {
return appendWebSearchTimeoutDetails([
pass(
'Web search backend',
formatDuckDuckGoReliabilityDetail('WEB_SEARCH_PROVIDER=ddg'),
),
])
}
const provider = getProviderChain(mode)[0]
const envVars = WEB_SEARCH_PROVIDER_ENV_VARS[mode] ?? []
const envVarLabel = formatEnvVarList(envVars)
const providerConfigured = Boolean(provider?.isConfigured())
if (mode === 'firecrawl') {
return appendWebSearchTimeoutDetails([buildFirecrawlWebSearchCheck()])
}
if (mode === 'custom') {
return [buildCustomWebSearchCheck(providerConfigured)]
}
if (!providerConfigured) {
return appendWebSearchTimeoutDetails([
fail(
'Web search backend',
`WEB_SEARCH_PROVIDER=${mode} but ${envVarLabel} is missing.`,
),
])
}
return appendWebSearchTimeoutDetails([
pass(
'Web search backend',
`WEB_SEARCH_PROVIDER=${mode}; ${envVarLabel} configured.`,
),
])
}
export function checkWebSearchEnv(): CheckResult[] {
return buildWebSearchEnvChecks()
}
function parseOptions(argv: string[]): CliOptions {
const options: CliOptions = {
json: false,
outFile: null,
}
for (let i = 0; i < argv.length; i++) {
const arg = argv[i]
if (arg === '--json') {
options.json = true
continue
}
if (arg === '--out') {
const next = argv[i + 1]
if (next && !next.startsWith('--')) {
options.outFile = next
i++
}
}
}
return options
}
export function formatReachabilityFailureDetail(
endpoint: string,
status: number,
responseBody: string,
request: {
transport: string
requestedModel: string
resolvedModel: string
},
): string {
const compactBody = safeDiagnosticText(
responseBody.trim().replace(/\s+/g, ' ').slice(0, 240),
'',
)
const base = `Unexpected status ${status} from ${redactUrlForDisplay(endpoint)}.`
const bodySuffix = compactBody ? ` Body: ${compactBody}` : ''
if (request.transport !== 'codex_responses' || status !== 400) {
return `${base}${bodySuffix}`
}
if (!/not supported.*chatgpt account/i.test(responseBody)) {
return `${base}${bodySuffix}`
}
const requestedModel = safeDisplayValue(request.requestedModel, 'the requested model')
const resolvedModel = safeDisplayValue(request.resolvedModel, 'the resolved model')
return `${base}${bodySuffix} Hint: model alias "${requestedModel}" resolved to "${resolvedModel}", which this ChatGPT account does not currently allow. Try "codexplan" or another entitled Codex model.`
}
export function readNodeExecutableVersion(
spawn = spawnSync,
): NodeExecutableVersionProbe {
const result = spawn('node', ['--version'], {
encoding: 'utf8',
})
if (result.error) {
return {
ok: false,
detail: `Unable to run \`node --version\`: ${result.error.message}. OpenClaude requires Node.js ${MIN_NODE_ENGINE_RANGE} on PATH.`,
}
}
if (result.status !== 0) {
const output = (result.stderr || result.stdout || '').trim()
const suffix = output ? `: ${output}` : `: exit code ${result.status ?? 'unknown'}`
return {
ok: false,
detail: `Unable to run \`node --version\`${suffix}. OpenClaude requires Node.js ${MIN_NODE_ENGINE_RANGE} on PATH.`,
}
}
const version = (result.stdout || result.stderr || '').trim()
if (!version) {
return {
ok: false,
detail: `Unable to read Node.js version from \`node --version\`. OpenClaude requires Node.js ${MIN_NODE_ENGINE_RANGE} on PATH.`,
}
}
return {
ok: true,
version,
}
}
export function checkNodeVersion(
raw: string | NodeExecutableVersionProbe = readNodeExecutableVersion(),
): CheckResult {
if (typeof raw !== 'string' && !raw.ok) {
return fail('Node.js version', raw.detail)
}
const versionCheck = checkSupportedNodeVersion(
typeof raw === 'string' ? raw : raw.version,
)
if (!versionCheck.ok) {
return fail('Node.js version', versionCheck.message)
}
return pass('Node.js version', versionCheck.version)
}
function checkBunRuntime(): CheckResult {
const bunVersion = (globalThis as { Bun?: { version?: string } }).Bun?.version
if (!bunVersion) {
return pass('Bun runtime', 'Not running inside Bun (this is acceptable for Node startup).')
}
return pass('Bun runtime', bunVersion)
}
function checkBuildArtifacts(): CheckResult {
const distCli = resolve(process.cwd(), 'dist', 'cli.mjs')
if (!existsSync(distCli)) {
return fail('Build artifacts', `Missing ${distCli}. Run: bun run build`)
}
return pass('Build artifacts', distCli)
}
export function isCliSandboxRuntimeStubbed(bundleText: string): boolean {
return bundleText.includes('native-stub:@anthropic-ai/sandbox-runtime')
}
type SandboxRuntimeCheckInput =
| {
inspectionError: unknown
}
| {
cliRuntimeStubbed: boolean
sandboxEnabled: boolean
failIfUnavailable: boolean
sandboxingEnabled: boolean
unavailableReason?: string
}
function formatUnknownError(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
export function buildSandboxRuntimeCheck(
input: SandboxRuntimeCheckInput,
): CheckResult {
if ('inspectionError' in input) {
return fail(
'Sandbox runtime',
`Unable to inspect CLI sandbox runtime: ${formatUnknownError(input.inspectionError)}`,
)
}
const effectiveBehavior = input.sandboxingEnabled
? 'enforcing'
: input.sandboxEnabled
? input.failIfUnavailable
? 'fail-closed'
: 'warning-only'
: 'disabled'
const detailParts = [
`CLI bundle: ${input.cliRuntimeStubbed ? 'stubbed' : 'real runtime'}`,
`sandbox.enabled: ${input.sandboxEnabled}`,
`failIfUnavailable: ${input.failIfUnavailable}`,
`effective behavior: ${effectiveBehavior}`,
]
const reason =
input.unavailableReason ??
(input.cliRuntimeStubbed && input.sandboxEnabled
? 'CLI bundle contains a no-op sandbox runtime stub'
: undefined)
if (reason) {
detailParts.push(`reason: ${reason}`)
}
const ok = !(
input.sandboxEnabled &&
input.failIfUnavailable &&
Boolean(reason)
)
return ok
? pass('Sandbox runtime', detailParts.join('; '))
: fail('Sandbox runtime', detailParts.join('; '))
}
function checkSandboxRuntime(): CheckResult {
const distCli = resolve(process.cwd(), 'dist', 'cli.mjs')
if (!existsSync(distCli)) {
return fail(
'Sandbox runtime',
`CLI bundle missing at ${distCli}. Run: bun run build`,
)
}
try {
const bundle = readFileSync(distCli, 'utf8')
return buildSandboxRuntimeCheck({
cliRuntimeStubbed: isCliSandboxRuntimeStubbed(bundle),
sandboxEnabled: SandboxManager.isSandboxEnabledInSettings(),
failIfUnavailable: SandboxManager.isSandboxRequired(),
sandboxingEnabled: SandboxManager.isSandboxingEnabled(),
unavailableReason: SandboxManager.getSandboxUnavailableReason(),
})
} catch (error) {
return buildSandboxRuntimeCheck({ inspectionError: error })
}
}
function isLocalBaseUrl(baseUrl: string): boolean {
return isProviderLocalUrl(baseUrl)
}
const GEMINI_DEFAULT_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/openai'
const MISTRAL_DEFAULT_BASE_URL = 'https://api.mistral.ai/v1'
const GITHUB_COPILOT_BASE = 'https://api.githubcopilot.com'
function currentSecretSource(): SecretValueSource {
return process.env as SecretValueSource
}
function safeDisplayValue(
value: string | null | undefined,
fallback: string,
): string {
return redactSecretValueForDisplay(value, currentSecretSource()) ?? fallback
}
function safeDiagnosticText(
value: string | null | undefined,
fallback: string,
): string {
return redactSecretSubstringsForDisplay(value, currentSecretSource()) ?? fallback
}
function safeBaseUrlDisplay(
value: string | null | undefined,
fallback: string,
): string {
if (!value) return fallback
return safeDisplayValue(redactUrlForDisplay(value), fallback)
}
function getOpenAICompatibleRouteId(baseUrl: string): string {
return (
resolveRouteIdFromBaseUrl(baseUrl) ??
resolveActiveRouteIdFromEnv(process.env) ??
'custom'
)
}
function getOpenAICompatibleCredentialContext(baseUrl: string): {
routeId: string
envVars: string[]
value: string | undefined
} {
const routeId = getOpenAICompatibleRouteId(baseUrl)
return {
routeId,
envVars: getRouteCredentialEnvVars(routeId),
value: getRouteCredentialValue(routeId, process.env),
}
}
function hasPlaceholderCredential(value: string | undefined): boolean {
return (value ?? '').split(',').some(part => part.trim() === 'SUA_CHAVE')
}
function currentBaseUrl(): string {
if (isTruthy(process.env.CLAUDE_CODE_USE_GEMINI)) {
return process.env.GEMINI_BASE_URL ?? GEMINI_DEFAULT_BASE_URL
}
if (isTruthy(process.env.CLAUDE_CODE_USE_MISTRAL)) {
return process.env.MISTRAL_BASE_URL ?? MISTRAL_DEFAULT_BASE_URL
}
if (isTruthy(process.env.CLAUDE_CODE_USE_GITHUB)) {
return process.env.OPENAI_BASE_URL ?? GITHUB_COPILOT_BASE
}
return process.env.OPENAI_BASE_URL ?? 'https://api.openai.com/v1'
}
function checkGeminiEnv(): CheckResult[] {
const results: CheckResult[] = []
const model = process.env.GEMINI_MODEL
const key = process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY
const baseUrl = process.env.GEMINI_BASE_URL ?? GEMINI_DEFAULT_BASE_URL
results.push(pass('Provider mode', 'Google Gemini provider enabled.'))
if (!model) {
results.push(pass('GEMINI_MODEL', `Not set. Default ${DEFAULT_GEMINI_MODEL} will be used.`))
} else {
results.push(pass('GEMINI_MODEL', safeDisplayValue(model, '')))
}
results.push(pass('GEMINI_BASE_URL', safeBaseUrlDisplay(baseUrl, '')))
if (!key) {
results.push(fail('GEMINI_API_KEY', 'Missing. Set GEMINI_API_KEY or GOOGLE_API_KEY.'))
} else {
results.push(pass('GEMINI_API_KEY', 'Configured.'))
}
return results
}
function checkMistralEnv(): CheckResult[] {
const results: CheckResult[] = []
const model = process.env.MISTRAL_MODEL
const key = process.env.MISTRAL_API_KEY
const baseUrl = process.env.MISTRAL_BASE_URL ?? MISTRAL_DEFAULT_BASE_URL
results.push(pass('Provider mode', 'Mistral provider enabled.'))
if (!model) {
results.push(pass('MISTRAL_MODEL', 'Not set. Default will be used at runtime.'))
} else {
results.push(pass('MISTRAL_MODEL', safeDisplayValue(model, '')))
}
results.push(pass('MISTRAL_BASE_URL', safeBaseUrlDisplay(baseUrl, '')))
if (!key) {
results.push(fail('MISTRAL_API_KEY', 'Missing. Set MISTRAL_API_KEY.'))
} else {
results.push(pass('MISTRAL_API_KEY', 'Configured.'))
}
return results
}
function checkGithubEnv(): CheckResult[] {
const results: CheckResult[] = []
const baseUrl = process.env.OPENAI_BASE_URL ?? GITHUB_COPILOT_BASE
results.push(pass('Provider mode', 'GitHub Models provider enabled.'))
const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN
if (!token?.trim()) {
results.push(fail('GITHUB_TOKEN', 'Missing. Set GITHUB_TOKEN or GH_TOKEN.'))
} else {
results.push(pass('GITHUB_TOKEN', 'Configured.'))
}
if (!process.env.OPENAI_MODEL) {
results.push(
pass(
'OPENAI_MODEL',
'Not set. Default github:copilot → openai/gpt-4.1 at runtime.',
),
)
} else {
results.push(pass('OPENAI_MODEL', safeDisplayValue(process.env.OPENAI_MODEL, '')))
}
results.push(pass('OPENAI_BASE_URL', safeBaseUrlDisplay(baseUrl, '')))
return results
}
export function checkOpenAIEnv(): CheckResult[] {
const results: CheckResult[] = []
const useGemini = isTruthy(process.env.CLAUDE_CODE_USE_GEMINI)
const useGithub = isTruthy(process.env.CLAUDE_CODE_USE_GITHUB)
const useMistral = isTruthy(process.env.CLAUDE_CODE_USE_MISTRAL)
const useOpenAI = isTruthy(process.env.CLAUDE_CODE_USE_OPENAI)
if (useGemini) {
return checkGeminiEnv()
}
if (useMistral) {
return checkMistralEnv()
}
if (useGithub && !useOpenAI) {
return checkGithubEnv()
}
if (!useOpenAI) {
results.push(pass('Provider mode', 'Anthropic login flow enabled (CLAUDE_CODE_USE_OPENAI is off).'))
return results
}
const request = resolveProviderRequest({
model: process.env.OPENAI_MODEL,
baseUrl: process.env.OPENAI_BASE_URL,
})
results.push(
pass(
'Provider mode',
request.transport === 'codex_responses'
? 'Codex responses backend enabled.'
: 'OpenAI-compatible provider enabled.',
),
)
if (!process.env.OPENAI_MODEL) {
results.push(pass('OPENAI_MODEL', 'Not set. Runtime fallback model will be used.'))
} else {
results.push(pass('OPENAI_MODEL', safeDisplayValue(process.env.OPENAI_MODEL, '')))
}
results.push(pass('OPENAI_BASE_URL', safeBaseUrlDisplay(request.baseUrl, '')))
if (request.transport === 'codex_responses') {
const credentials = resolveCodexApiCredentials(process.env)
if (!credentials.apiKey) {
const authHint = credentials.authPath
? `Missing CODEX_API_KEY and no usable auth.json at ${credentials.authPath}.`
: 'Missing CODEX_API_KEY and auth.json fallback.'
results.push(fail('CODEX auth', authHint))
} else if (!credentials.accountId) {
results.push(fail('CHATGPT_ACCOUNT_ID', 'Missing chatgpt_account_id in Codex auth.'))
} else {
const detail = credentials.source === 'env'
? 'Using CODEX_API_KEY.'
: `Using ${credentials.authPath}.`
results.push(pass('CODEX auth', detail))
}
return results
}
const credentialContext = getOpenAICompatibleCredentialContext(request.baseUrl)
const providerCredential = credentialContext.value
const credentialLabel =
credentialContext.envVars.length > 0
? credentialContext.envVars.join(' or ')
: 'OPENAI_API_KEY'
const githubToken = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN
const hasGithubRouteCredential =
credentialContext.routeId === 'github' && Boolean(githubToken?.trim())
const openAIState = resolveOpenAICredentialEnvState(process.env)
const hasOpenAIFallback =
credentialContext.envVars.includes('OPENAI_API_KEYS') ||
credentialContext.envVars.includes('OPENAI_API_KEY')
const hasPlaceholderProviderCredential = credentialContext.envVars.some(envVar => {
if (
hasOpenAIFallback &&
(envVar === 'OPENAI_API_KEYS' || envVar === 'OPENAI_API_KEY')
) {
return openAIState.invalid && openAIState.envVar === envVar
}
return hasPlaceholderCredential(process.env[envVar])
})
if (
hasPlaceholderCredential(providerCredential) ||
hasPlaceholderProviderCredential
) {
results.push(fail(credentialLabel, 'Placeholder value detected: SUA_CHAVE.'))
} else if (
!providerCredential &&
!isLocalBaseUrl(request.baseUrl) &&
!hasGithubRouteCredential
) {
results.push(fail(credentialLabel, `Missing key for non-local provider URL. Set ${credentialLabel}.`))
} else if (!providerCredential && hasGithubRouteCredential) {
results.push(
pass('OPENAI_API_KEY', 'Not set; GITHUB_TOKEN/GH_TOKEN will be used for GitHub Models.'),
)
} else if (!providerCredential) {
results.push(pass(credentialLabel, 'Not set (allowed for local providers like Atomic Chat/Ollama/LM Studio).'))
} else {
results.push(pass(credentialLabel, 'Configured.'))
}
return results
}
async function checkBaseUrlReachability(): Promise<CheckResult> {
const useGemini = isTruthy(process.env.CLAUDE_CODE_USE_GEMINI)
const useOpenAI = isTruthy(process.env.CLAUDE_CODE_USE_OPENAI)
const useGithub = isTruthy(process.env.CLAUDE_CODE_USE_GITHUB)
const useMistral = isTruthy(process.env.CLAUDE_CODE_USE_MISTRAL)
if (!useGemini && !useOpenAI && !useGithub && !useMistral) {
return pass('Provider reachability', 'Skipped (OpenAI-compatible mode disabled).')
}
if (useGithub && !useOpenAI) {
return pass(
'Provider reachability',
'Skipped for GitHub Models (inference endpoint differs from OpenAI /models probe).',
)
}
const geminiBaseUrl = 'https://generativelanguage.googleapis.com/v1beta/openai'
const resolvedBaseUrl = useGemini
? (process.env.GEMINI_BASE_URL ?? geminiBaseUrl)
: undefined
const request = resolveProviderRequest({
model: process.env.OPENAI_MODEL,
baseUrl: resolvedBaseUrl ?? process.env.OPENAI_BASE_URL,
})
const endpoint = request.transport === 'codex_responses'
? `${request.baseUrl}/responses`
: `${request.baseUrl}/models`
const redactedEndpoint = redactUrlForDisplay(endpoint)
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 4000)
try {
const headers: Record<string, string> = {}
let method = 'GET'
let body: string | undefined
if (request.transport === 'codex_responses') {
const credentials = resolveCodexApiCredentials(process.env)
if (credentials.apiKey) {
headers.Authorization = `Bearer ${credentials.apiKey}`
}
if (credentials.accountId) {
headers['chatgpt-account-id'] = credentials.accountId
}
headers['Content-Type'] = 'application/json'
headers.originator = 'openclaude'
method = 'POST'
body = JSON.stringify({
model: request.resolvedModel,
instructions: 'Runtime doctor probe.',
input: [
{
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'ping' }],
},
],
store: false,
stream: true,
})
} else if (useGemini && (process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY)) {
headers.Authorization = `Bearer ${process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY}`
} else if (useMistral && process.env.MISTRAL_API_KEY) {
headers.Authorization = `Bearer ${process.env.MISTRAL_API_KEY}`
} else {
const credential = getOpenAICompatibleCredentialContext(request.baseUrl).value
if (credential) {
headers.Authorization = `Bearer ${credential}`
}
}
const response = await fetch(endpoint, {
method,
headers,
body,
signal: controller.signal,
})
if (response.status === 200 || response.status === 401 || response.status === 403) {
return pass(
'Provider reachability',
`Reached ${redactedEndpoint} (status ${response.status}).`,
)
}
const responseBody = await response.text().catch(() => '')
const detail = formatReachabilityFailureDetail(
endpoint,
response.status,
responseBody,
request,
)
return fail(
'Provider reachability',
detail,
)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return fail(
'Provider reachability',
`Failed to reach ${redactedEndpoint}: ${message}`,
)
} finally {
clearTimeout(timeout)
}
}
async function checkProviderGenerationReadiness(): Promise<CheckResult> {
const useGemini = isTruthy(process.env.CLAUDE_CODE_USE_GEMINI)
const useOpenAI = isTruthy(process.env.CLAUDE_CODE_USE_OPENAI)
const useGithub = isTruthy(process.env.CLAUDE_CODE_USE_GITHUB)
const useMistral = isTruthy(process.env.CLAUDE_CODE_USE_MISTRAL)
if (!useGemini && !useOpenAI && !useGithub && !useMistral) {
return pass('Provider generation readiness', 'Skipped (OpenAI-compatible mode disabled).')
}
if (useGithub && !useOpenAI) {
return pass(
'Provider generation readiness',
'Skipped for GitHub Models (runtime generation uses a different endpoint flow).',
)
}
if (useGemini || useMistral) {
return pass(
'Provider generation readiness',
'Skipped for managed provider mode.',
)
}
if (!useOpenAI) {
return pass('Provider generation readiness', 'Skipped (OpenAI-compatible mode disabled).')
}
const request = resolveProviderRequest({
model: process.env.OPENAI_MODEL,
baseUrl: process.env.OPENAI_BASE_URL,
})
if (request.transport === 'codex_responses') {
return pass(
'Provider generation readiness',
'Skipped for Codex responses (reachability probe already performs a lightweight generation request).',
)
}
if (!isLocalBaseUrl(request.baseUrl)) {
return pass('Provider generation readiness', 'Skipped for non-local provider URL.')
}
const localProviderLabel = getLocalOpenAICompatibleProviderLabel(request.baseUrl)
if (localProviderLabel !== 'Ollama') {
return pass(
'Provider generation readiness',
`Skipped for ${localProviderLabel} (no provider-specific generation probe).`,
)
}
const readiness = await probeOllamaGenerationReadiness({
baseUrl: request.baseUrl,
model: request.requestedModel,
})
if (readiness.state === 'ready') {
return pass(
'Provider generation readiness',
`Generated a test response with ${safeDisplayValue(readiness.probeModel ?? request.requestedModel, 'the requested model')}.`,
)
}
if (readiness.state === 'unreachable') {
return fail(
'Provider generation readiness',
`Could not reach Ollama at ${redactUrlForDisplay(request.baseUrl)}.`,
)
}
if (readiness.state === 'no_models') {
return fail(
'Provider generation readiness',
'Ollama is reachable, but no installed models were found. Pull a model first (for example: ollama pull qwen2.5-coder:7b).',
)
}
const detail = safeDiagnosticText(readiness.detail, '')
const detailSuffix = detail ? ` Detail: ${detail}.` : ''
return fail(
'Provider generation readiness',
`Ollama is reachable, but generation failed for ${safeDisplayValue(readiness.probeModel ?? request.requestedModel, 'the requested model')}.${detailSuffix}`,
)
}
function isAtomicChatUrl(baseUrl: string): boolean {
try {
const parsed = new URL(baseUrl)
return parsed.port === '1337' && isLocalBaseUrl(baseUrl)
} catch {
return false
}
}
function checkOllamaProcessorMode(): CheckResult {
if (
!isTruthy(process.env.CLAUDE_CODE_USE_OPENAI) ||
isTruthy(process.env.CLAUDE_CODE_USE_GEMINI) ||
isTruthy(process.env.CLAUDE_CODE_USE_GITHUB) ||
isTruthy(process.env.CLAUDE_CODE_USE_MISTRAL)
) {
return pass('Ollama processor mode', 'Skipped (OpenAI-compatible mode disabled).')
}
const baseUrl = currentBaseUrl()
if (!isLocalBaseUrl(baseUrl)) {
return pass('Ollama processor mode', 'Skipped (provider URL is not local).')
}
if (isAtomicChatUrl(baseUrl)) {
return pass('Ollama processor mode', 'Skipped (Atomic Chat local provider detected, not Ollama).')
}
const result = spawnSync('ollama', ['ps'], {
cwd: process.cwd(),
encoding: 'utf8',
shell: true,
})
if (result.status !== 0) {
const detail = (result.stderr || result.stdout || 'Unable to run ollama ps').trim()
return pass('Ollama processor mode', `Native CLI check failed (${detail}). Assuming valid Docker/remote backend since HTTP ping passed.`)
}
const output = (result.stdout || '').trim()
if (!output) {
return fail('Ollama processor mode', 'ollama ps returned empty output.')
}
const lines = output.split(/\r?\n/).map(line => line.trim()).filter(Boolean)
const modelLine = lines.find(line => line.includes(':') && !line.startsWith('NAME'))
if (!modelLine) {
return pass('Ollama processor mode', 'No loaded model found (run a prompt first).')
}
if (modelLine.includes('CPU')) {
return pass('Ollama processor mode', 'Detected CPU mode. This is valid but can be slow for larger models.')
}
return pass('Ollama processor mode', `Detected non-CPU mode: ${modelLine}`)
}
export function serializeSafeEnvSummary(): Record<string, string | boolean> {
if (isTruthy(process.env.CLAUDE_CODE_USE_GEMINI)) {
return {
CLAUDE_CODE_USE_GEMINI: true,
GEMINI_MODEL: safeDisplayValue(process.env.GEMINI_MODEL, `(unset, default: ${DEFAULT_GEMINI_MODEL})`),
GEMINI_BASE_URL: safeBaseUrlDisplay(process.env.GEMINI_BASE_URL ?? 'https://generativelanguage.googleapis.com/v1beta/openai', ''),
GEMINI_API_KEY_SET: Boolean(process.env.GEMINI_API_KEY ?? process.env.GOOGLE_API_KEY),
}
}
if (isTruthy(process.env.CLAUDE_CODE_USE_MISTRAL)) {
return {
CLAUDE_CODE_USE_MISTRAL: true,
MISTRAL_MODEL: safeDisplayValue(process.env.MISTRAL_MODEL, '(unset, default: devstral-latest)'),
MISTRAL_BASE_URL: safeBaseUrlDisplay(process.env.MISTRAL_BASE_URL ?? 'https://api.mistral.ai/v1', ''),
MISTRAL_API_KEY_SET: Boolean(process.env.MISTRAL_API_KEY),
}
}
if (
isTruthy(process.env.CLAUDE_CODE_USE_GITHUB) &&
!isTruthy(process.env.CLAUDE_CODE_USE_OPENAI)
) {
return {
CLAUDE_CODE_USE_GITHUB: true,
OPENAI_MODEL:
safeDisplayValue(
process.env.OPENAI_MODEL,
'(unset, default: github:copilot → openai/gpt-4.1)',
),
OPENAI_BASE_URL:
safeBaseUrlDisplay(process.env.OPENAI_BASE_URL ?? GITHUB_COPILOT_BASE, ''),
GITHUB_TOKEN_SET: Boolean(
process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN,
),
}
}
const request = resolveProviderRequest({
model: process.env.OPENAI_MODEL,
baseUrl: process.env.OPENAI_BASE_URL,
})
const credentialContext = getOpenAICompatibleCredentialContext(request.baseUrl)
return {
CLAUDE_CODE_USE_OPENAI: isTruthy(process.env.CLAUDE_CODE_USE_OPENAI),
OPENAI_MODEL: safeDisplayValue(process.env.OPENAI_MODEL, '(unset)'),
OPENAI_BASE_URL: safeBaseUrlDisplay(request.baseUrl, ''),
OPENAI_API_KEY_SET: Boolean(process.env.OPENAI_API_KEY),
PROVIDER_API_KEY_SET: Boolean(credentialContext.value),
CODEX_API_KEY_SET: Boolean(resolveCodexApiCredentials(process.env).apiKey),
}
}
function printResults(results: CheckResult[]): void {
for (const result of results) {
const icon = result.ok ? 'PASS' : 'FAIL'
const suffix = result.detail ? ` - ${result.detail}` : ''
console.log(`[${icon}] ${result.label}${suffix}`)
}
}
function writeJsonReport(
options: CliOptions,
results: CheckResult[],
): void {
const envSummary = serializeSafeEnvSummary()
const payload = {
timestamp: new Date().toISOString(),
cwd: process.cwd(),
summary: {
total: results.length,
passed: results.filter(result => result.ok).length,
failed: results.filter(result => !result.ok).length,
},
env: envSummary,
results,
}
if (options.json) {
console.log(
JSON.stringify(
{
timestamp: payload.timestamp,
cwd: payload.cwd,
summary: payload.summary,
env: '[redacted in console JSON output; use --out-file for the full report]',
results: payload.results,
},
null,
2,
),
)
}
if (options.outFile) {
const outputPath = resolve(process.cwd(), options.outFile)
mkdirSync(dirname(outputPath), { recursive: true })
writeFileSync(outputPath, JSON.stringify(payload, null, 2), 'utf8')
if (!options.json) {
console.log(`Report written to ${outputPath}`)
}
}
}
async function main(): Promise<void> {
const options = parseOptions(process.argv.slice(2))
const results: CheckResult[] = []
const configModule = await import('../src/utils/config.js')
configModule.enableConfigs()
const { applySafeConfigEnvironmentVariables } = await import('../src/utils/managedEnv.js')
applySafeConfigEnvironmentVariables()
const { hydrateGithubModelsTokenFromSecureStorage } = await import('../src/utils/githubModelsCredentials.js')
hydrateGithubModelsTokenFromSecureStorage()
results.push(checkNodeVersion())
results.push(checkBunRuntime())
results.push(checkBuildArtifacts())
results.push(checkSandboxRuntime())
const globalConfig = configModule.getGlobalConfig()
results.push(
...buildMemoryGuardChecks({
autoCompactEnabled: globalConfig.autoCompactEnabled,
maxMessagesCompactionThreshold:
globalConfig.maxMessagesCompactionThreshold,
}),
)
results.push(...checkWebSearchEnv())
results.push(...checkOpenAIEnv())
results.push(await checkBaseUrlReachability())
results.push(await checkProviderGenerationReadiness())
results.push(checkOllamaProcessorMode())
if (!options.json) {
printResults(results)
}
writeJsonReport(options, results)
const hasFailure = results.some(result => !result.ok)
if (hasFailure) {
process.exitCode = 1
return
}
if (!options.json) {
console.log('\nRuntime checks completed successfully.')
}
}
if (import.meta.main) {
await main()
}
export {}