* fix(autocompact): retry circuit breaker after cooldown
* fix: honor auto-compact cooldown fallback
Derive the circuit breaker retry time from lastFailureAtMs plus the configured cooldown when nextRetryAtMs is missing or invalid. Also clear SDK auto-compact tracking after manual compact boundaries and cover both paths with regressions.
* test: make auto-compact cooldown fixture portable
* test: extend auto-compact cooldown fixture timeout
The OpenGateway preset was missing `apiKeyEnvVars`, so credential
resolution fell back to the descriptor's `credentialEnvVars` chain
which includes `OPENAI_API_KEY`. If a user had `OPENAI_API_KEY` set
for a different provider, the add-provider flow silently pre-populated
the draft key and skipped the API key input screen entirely.
Add an explicit `apiKeyEnvVars: ['OPENGATEWAY_API_KEY']` to the preset
so the UI only auto-fills from the provider-specific env var. The
runtime setup/validation still falls back to `OPENAI_API_KEY` for
existing configs.
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
Remote Ollama servers (host outside the loopback / RFC1918 range, or
on a domain like ollama.corp.example.com) hit the OPENAI provider
validation gate that requires OPENAI_API_KEY whenever the base URL is
not local. Ollama doesn't need an API key, so the user has to invent a
phantom value to get past startup.
Extend the bypass to recognise likely-Ollama base URLs in addition to
local URLs:
- port 11434 (Ollama default) on any host
- 'ollama' substring in hostname or pathname
isLikelyOllamaEndpoint already encoded these heuristics for tool-call
gating in providerConfig.ts; export it and reuse so the rules stay in
one place.
Fixes#369
Ensures package.json scripts (dev, start), scripts/provider-launch.ts,
and Dockerfile route node executions through the bin/openclaude launcher
rather than calling node directly on dist/cli.mjs.
This resolves PR feedback:
1. Preserves the robust launcher relaunch guard, GC exposure, and test
coverage already merged on main (from #1242).
2. Prevents hardcoded heap caps (--max-old-space-size=8192) from overriding
user-provided NODE_OPTIONS or OPENCLAUDE_NODE_MAX_OLD_SPACE_SIZE_MB
settings during development, start, or containerized runs.
Co-authored-by: daltoncoder <daltoncoder@example.com>
* fix(thinking): disable thinking for unsupported Ollama models
Fixes#1371
- Adds central `shouldUseThinkingForModel` gate that checks the actual route and model descriptor.
- Disables thinking parameters for the Ollama route when the model is unknown or unsupported.
- Updates API requests to evaluate the actual retry model against the capability gate instead of the initial request model.
- Adds targeted tests for Ollama logic and shim payloads.
* test(thinking): cover Ollama thinking gate
`codexspark` / `gpt-5.3-codex-spark` deliver the complete function-call
arguments only through the terminal `response.function_call_arguments.done`
event (and sometimes only on `response.output_item.done`), with zero
`response.function_call_arguments.delta` events in between. The Anthropic-
compat stream adapter ignored both `done` channels for arguments, so the
tool_use block closed with `input: {}` and the routed agent's Glob/Bash
call failed validation with "required parameter X is missing" (#1259).
Fixes:
1. Track `emittedArgs` per tool block (initial seeded from
`output_item.added.item.arguments` when present).
2. Handle `response.function_call_arguments.done` — emit the full
`arguments` string as an `input_json_delta` if no deltas streamed.
3. Backstop in `response.output_item.done` for backends that skip the
dedicated arguments-done event entirely.
The `!toolBlock.emittedArgs` guard on both done branches prevents double
emission when deltas were already streamed (the common path).
Tests cover:
- args delivered only via `function_call_arguments.done`
- args delivered only via `output_item.done` (no arguments.done at all)
- delta path still wins; done events do not duplicate the JSON
Closes#1259
* feat: configure API retry backoff
Add OpenClaude-branded retry controls for retryable API failures.
- Replace the old CLAUDE_CODE_MAX_RETRIES config with OPENCLAUDE_MAX_RETRIES
- Allow OPENCLAUDE_MAX_RETRIES=0 to disable retries after the initial request
- Cap retry attempts at 100 and invalid values fall back to the default of 10
- Add OPENCLAUDE_RETRY_DELAY_MS to configure the exponential backoff base for APIs that omit Retry-After
- Keep Retry-After precedence over configured retry delay
- Document both settings in .env.example and advanced setup docs
- Add focused retry configuration tests for defaults, invalid values, caps, zero retries, configured delay, and Retry-After precedence
Validation:
- bun test src/services/api/withRetry.test.ts
- bun run build
* Honor legacy max retries env var
Add compatibility fallback from CLAUDE_CODE_MAX_RETRIES when OPENCLAUDE_MAX_RETRIES is unset.
Document the deprecated fallback and cover precedence behavior in retry configuration tests.
---------
Co-authored-by: JATMN <12479882+jatmn@users.noreply.github.com>
* fix: route MiniMax through Anthropic-compatible API
Switch MiniMax provider setup away from the OpenAI-compatible shim and onto the Anthropic-compatible endpoint. Update env-only, provider flag, and saved profile paths to use ANTHROPIC_* while preserving legacy OPENAI_MODEL as a migration fallback.
Adjust MiniMax M2 context metadata so shared descriptors use the gateway-safe 196608 window and the direct MiniMax catalog overrides to the documented 204800 window. Extend runtime context lookup to anthropic-proxy routes so compact budgeting uses the direct provider metadata.
Update MiniMax client, provider profile, provider flag, context, and auto-compact tests for the Anthropic-compatible route and provider-specific compact limits.
* test: cover MiniMax provider manager paths
Update ProviderManager test fixtures so MiniMax uses the Anthropic-compatible endpoint instead of the old OpenAI-compatible /v1 endpoint.
Add coverage for the /provider add flow to assert MiniMax saves provider=minimax, endpoint https://api.minimax.io/anthropic, and displays the Anthropic-compatible API provider type.
Add edit-flow coverage to ensure existing MiniMax profiles remain on the Anthropic-compatible provider path and continue hiding OpenAI-only advanced fields.
* test: isolate MiniMax env-only coverage
Harden MiniMax client and compact tests against ambient CI provider env such as OPENAI_API_KEY, ANTHROPIC_BASE_URL, and provider-profile markers.
The compact budget test now explicitly clears competing provider flags before asserting direct MiniMax metadata, preventing CI-level OpenAI credentials from masking env-only MiniMax route detection.
* test: reset provider env inside MiniMax client cases
Make each env-only MiniMax client test clear competing provider flags, OpenAI/XAI keys, Anthropic env, and saved-profile markers before setting MINIMAX_API_KEY.
This keeps the Anthropic-compatible MiniMax route assertions independent of CI-level process env that can otherwise mask env-only provider detection in the full serial suite.
* fix: honor explicit MiniMax routing intent
Route MiniMax env-only requests by explicit MiniMax model/base intent even when generic OpenAI-compatible environment variables are present, while preserving non-MiniMax base URL conflicts.
Use the resolved MiniMax env-only path for Anthropic SDK key selection so stale provider classification or Bun module mocks cannot fall back to an Anthropic test key.
Harden MiniMax compact coverage against leaked env overrides and prior autoCompact module mocks, and cover the ambient OpenAI/XAI env regression.
* test: clean up compression autoCompact mocks
Restore Bun module mocks after compression test files so their deterministic autoCompact window does not leak into later compact tests in full-suite order.
Verified the MiniMax compact regression now passes after the compression suites and in the full local test log.
* test: avoid autoCompact module mocks in compression tests
Replace the compression suites' top-level Bun module mocks for autoCompact/config with real test config and env controls. This avoids Bun 1.3.11 leaking a mocked effective context window into the later MiniMax compact test in full-suite order.
Verified compression-before-compact and MiniMax focused suites pass locally.
* test: allow capped MiniMax compact reservation
CI enables the output-token slot-reservation cap, so MiniMax's direct 204,800 context can produce a 196,800 effective compact window instead of the uncapped 184,800. Keep the test focused on direct MiniMax context metadata while accepting either reservation state.
* fix: address MiniMax review findings
Treat env-only provider routes such as direct MiniMax as complete startup provider selections so saved profiles do not override explicit MINIMAX_API_KEY/ANTHROPIC_* env.
Stop advertising direct MiniMax benchmark support through the OpenAI-compatible benchmark path, and add regression coverage for the unsupported direct MiniMax benchmark env.
* fix: classify MiniMax profile startup correctly
Recognize MiniMax when /provider loads it through the Anthropic-compatible env shape using ANTHROPIC_BASE_URL, ANTHROPIC_MODEL, and ANTHROPIC_API_KEY.
Label MiniMax correctly on the startup screen and skip the Anthropic custom-key approval prompt when the resolved provider is not using the Anthropic account flow.
Add regressions for route metadata, legacy provider classification, account-flow bypass, and startup display for Anthropic-compatible MiniMax profiles.
* fix: include Anthropic key in provider secret source
Allow MiniMax profile redaction to include ANTHROPIC_API_KEY in the SecretValueSource type used by sanitizeProviderConfigValue.
This fixes the PR-specific TS2353 reported by review while keeping the MiniMax Anthropic-compatible key alias redacted alongside MINIMAX_API_KEY.
Validation: bun test --max-concurrency=1 src\utils\providerProfiles.test.ts src\utils\providerFlag.test.ts src\utils\model\providers.test.ts src\integrations\routeMetadata.test.ts; bun run typecheck still has existing repo-wide errors, with no providerProfile.ts matches.
* test: stabilize tool history compression smoke
Add a narrow compression-enabled override for tests so the compression suites do not depend on shared global config state from the full Bun runner.
Pass explicit effective context windows in direct compression tests and use catalog-backed models in shim compression tests to avoid env-capped tier drift.
Verified with focused compression tests, full bun test --max-concurrency=1, and bun run build.
* test: stabilize Orama corruption recovery assertion
Verify the quarantined corrupted Orama file from the actual persistence directory returned by getOramaPersistencePath, instead of assuming the config-dir projects root used by the full CI runner.
Verified with the failing KnowledgeGraph stress test, compression smoke suites, full bun test --max-concurrency=1, and bun run build.
* fix: refresh MiniMax compact branch
Merge upstream/main into fix/minimax-compact so PR #1154 is current with the target branch.
Also fix two branch-local FetchType test casts that surfaced during typecheck scanning of the MiniMax/xAI fallback tests.
Validation: bun test --max-concurrency=1 src/utils/providerProfiles.test.ts src/utils/providerFlag.test.ts src/utils/model/providers.test.ts src/integrations/routeMetadata.test.ts src/services/api/client.test.ts src/services/compact/autoCompact.test.ts src/utils/model/benchmark.test.ts; bun run build.
Relaunch the package executable before loading dist/cli.mjs so OpenClaude starts with an effective V8 heap cap instead of setting NODE_OPTIONS after the current process has already started.
The launcher now adds a default 8192 MB max-old-space-size and --expose-gc when they are missing, preserves flags supplied through process.execArgv or NODE_OPTIONS, and provides OPENCLAUDE_DISABLE_HEAP_RELAUNCH plus OPENCLAUDE_NODE_MAX_OLD_SPACE_SIZE_MB escape hatches.
Update the headless loop GC hook to use Node global.gc when the launcher exposed it, while preserving the existing Bun.gc path. Clarify the entrypoint NODE_OPTIONS comment so it reflects child-process propagation rather than current-process heap sizing.
Add scripts/openclaude-bin-heap.test.ts to guard launcher ordering and user override handling.
Validation: bun test scripts/openclaude-bin-heap.test.ts src/entrypoints/cli.test.ts; node bin/openclaude --version returned 0.13.0 (OpenClaude). Earlier full build passed after bun install --frozen-lockfile. bun run typecheck remains blocked by existing repo-wide type errors unrelated to this change.
* feat(safety): warn at startup when 3P provider runs in a permissive mode
Issue #244 finding 1: `modelSupportsAutoMode` returns `false` for every
non-firstParty provider (betas.ts:166), so the AI safety classifier that
reviews tool calls in context never runs for OpenAI/Gemini/Ollama/etc.
users — even when they are in `acceptEdits` or `bypassPermissions` mode,
where the per-tool consent prompt is suppressed. They get the consent
shortcut without the safety net, with no indication that the net is off.
Adds a `thirdPartyPermissiveModeNotice` to `statusNoticeDefinitions`
that fires when:
- active permission mode ∈ {acceptEdits, bypassPermissions}, AND
- the active model does NOT support auto-mode (covers all 3P), AND
- `getAPIProvider() !== 'firstParty'`
Plumbing: `StatusNoticeContext` grows `permissionMode` and `mainLoopModel`
fields, populated in `StatusNotices.tsx` via `useAppState`. The two
existing helpers (`modelSupportsAutoMode`, `getAPIProvider`) are reused —
no new policy logic, just a visible label on an existing gap.
Refs #244
* feat(safety): warn when --dangerously-skip-permissions runs without a sandbox
Issue #244 finding 2: the sandbox gate (Docker/Bubblewrap container +
no internet) that conditions `--dangerously-skip-permissions` is
employee-only (`isAntEmployee()`); external users — every OpenClaude
user — bypass the gate entirely. Combined with finding 1 (no AI
classifier on 3P), the flag becomes "run any command with full internet
access, no consent prompt, no safety net" with zero visible warning.
Adds `dangerouslySkipPermissionsNotice` to the startup notice list. It
fires when either:
- `process.argv` contains `--dangerously-skip-permissions`, OR
- the resolved permission mode is `bypassPermissions` (covers
settings.json `defaultMode` and runtime toggles too)
argv detection means the notice surfaces from the first frame, before
any AppState propagation, so the user sees the warning during the same
session in which they passed the flag — not on the next launch.
This does not change enforcement (that's a policy call for maintainers,
not a fork to ship). It surfaces an existing risk the CLI was silent
about.
Refs #244
* test(safety): cover both 3P-safety status notices
Eight cases fence the new contract:
- 3P + acceptEdits + classifier-off → fire
- 3P + bypassPermissions → fire
- 3P + default mode → suppressed (consent prompt still active)
- firstParty Anthropic + acceptEdits → suppressed (classifier present)
- 3P + acceptEdits + classifier-supported model → suppressed (defensive
branch in case future 3P models gain classifier support)
- --dangerously-skip-permissions in argv → fire
- bypassPermissions mode (e.g. settings defaultMode) → fire
- default mode without the flag → suppressed
mock.module + nonced re-import isolates the provider/classifier checks
per case so a misbehaving global cannot leak between tests.
`--json-schema` failed with "Failed to provide valid structured output
after maximum retries" whenever the schema's root `type` wasn't `object`
(top-level arrays, strings, etc.). Object schemas worked, and arrays
nested inside objects worked — only top-level non-objects broke.
Root cause: the Anthropic tool_use block requires the `input` field to
be a JSON object (the SDK types it as `Record<string, unknown>`). When
SyntheticOutputTool used the user's array schema as `inputJSONSchema`
directly, the model had no valid object shape to emit and returned `{}`,
which failed Ajv validation on every retry until the retry budget ran
out.
Fix: detect non-object root schemas in `buildSyntheticOutputTool` and
wrap them as `{ type: 'object', properties: { result: <orig> },
required: ['result'], additionalProperties: false }`. After validation,
unwrap `input.result` before emitting `structured_output` so the CLI
prints the same array (or string, number, etc.) the user asked for.
Object roots pass through untouched.
Tests cover:
- top-level array root: schema wrapped, output unwrapped to plain array
- top-level string root: same wrap/unwrap path
- object root: pass-through unchanged
- inner-schema violations still raise the schema-mismatch error
Closes#1256
* fix(bash): preserve captured stdout in error message on non-zero exit
Match PowerShellTool's pattern of passing captured output on the stdout
slot of ShellError so getErrorParts() surfaces the command output
alongside "Exit code N". The previous throw buried the merged output in
the stderr slot with stdout=''; the data still reached formatError
through the spread, but the swapped slots made it easy to lose output
if downstream consumers only inspected error.stdout.
Also drops the dead stdoutAccumulator.append("Exit code N") — the throw
above it discards the accumulator and getErrorParts() already prepends
"Exit code N" from error.code.
Adds regression tests covering the failure scenarios from the issue:
captured stdout/stderr appear in the formatted error, command-not-found
messages reach the surface, and empty-output failures still emit the
exit code.
Closes#1231
* test(bash): build full permission context for error-output tests
The hand-rolled `{ mode: 'default' }` context failed inside
`resetCwdIfOutsideProject` (reads `additionalWorkingDirectories`)
before assertions ran, so the regression was not actually exercised.
Use `getEmptyToolPermissionContext()` like the other BashTool tests.
Refs #1231.
DeepSeek, Moonshot/Kimi, Z.AI GLM, and Xiaomi MiMo require `reasoning_content`
echoed back on assistant messages in thinking mode (`preserveReasoningContent`
in the openai-shim runtime config). The shim populates that field from the
`thinking` content block on the Anthropic-side message, so stripping those
blocks during 3P resume left no source and the provider 400'd with:
The `reasoning_content` in the thinking mode must be passed back to the
API.
Skip the 3P thinking strip when the active route/model resolves to a shim
config with `preserveReasoningContent: true`. Other 3P providers (generic
OpenAI, etc.) keep the original strip from #248 finding 5.
Closes#957.
OpenRouter (and other quota-billed OpenAI-compat gateways) reply with
HTTP 402 when the caller has fewer credits than the requested
max_tokens would consume. The body includes the affordable cap:
This request requires more credits, or fewer max_tokens. You
requested up to 32000 tokens, but can only afford 27342.
Previously this surfaced as a fatal API error and the user had to
guess what value to put in `CLAUDE_CODE_MAX_OUTPUT_TOKENS` to make the
request fit. Now `withRetry` parses the affordable number out of the
message and retries once with `maxTokensOverride = affordable` —
mirroring the existing context-overflow retry path. A single stderr
line tells the user output was clamped so they can top up credits if
they want the full budget back.
Single-shot adjustment (gated by `retryContext.maxTokensOverride ===
undefined`) so an unrelated subsequent 402 doesn't loop.
Also fixes pre-existing test-fixture mock leak: the `providers.js`
stub didn't include `isFirstPartyAnthropicBaseUrl` /
`usesAnthropicAccountFlow` / `isGithubNativeAnthropicMode`, so the
entire `withRetry.test.ts` file errored on import.
Closes#1125
Validate XAA IdP callback state before processing provider errors or authorization codes.
Keep invalid-state callbacks non-terminal so forged error requests cannot close the active local callback server.
Add focused regression coverage for error callbacks without state, provider errors with matching state, and valid authorization codes.
* feat(xai): add xAI/Grok OAuth provider (browser + device-code)
Sign in to xAI with your account instead of an API key. Inference uses
the access token as a Bearer to api.x.ai/v1 (same surface as XAI_API_KEY)
with automatic refresh ~60s before expiry.
CLI: openclaude auth xai {login|device|status|logout}
UI: /login → 3rd-party platform → xAI OAuth (Grok)
Implementation mirrors openclaw's xai-oauth (shared client_id, PKCE,
OIDC discovery against auth.x.ai, trusted-host gating, refresh_token).
The loopback callback server (127.0.0.1:56121) explicitly echoes CORS
preflight for auth.x.ai / accounts.x.ai so xAI's browser-side push
reaches us; if the loopback still fails (firewall, remote host), users
can paste the code shown on xAI's auth page directly into the CLI or
the ProviderManager input.
Also adds grok-code-fast-1 to the xAI catalog and the x-grok-conv-id
prompt-caching header (mirrors hermes-agent).
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(xai): satisfy provider validation with OAuth + drop raw timeout signal
Two P1 review items from the xAI OAuth PR:
1. After signing in with `openclaude auth xai login` (or via the
ProviderManager), the xAI vendor still required XAI_API_KEY because
validation only checked `credentialEnvVars: ['XAI_API_KEY']`. A user
running `openclaude -p` could exit on the missing-key warning before
openaiShim ever resolved the stored OAuth token. Adds a new
`xai-credential` validation kind that accepts, in order:
a. XAI_API_KEY (legacy / explicit override)
b. XAI_CREDENTIAL_SOURCE=oauth env marker (set by the saved
OAuth profile when its env is applied at startup)
c. stored OAuth credentials in secure storage (covers the
first-process gap before applySavedProfile runs)
Resolver (c) is injectable so tests aren't sensitive to the
developer's actual login state. Adds regression tests for all four
paths (API key, env marker, stored creds, none).
2. `fetchXaiOAuthDiscovery` used `AbortSignal.timeout(...)` directly,
which the `scripts/no-raw-abort-signal-timeout.test.ts` repo guard
forbids (raw timeout signals leak timers in Bun). Routes through the
existing `createCombinedAbortSignal` helper with proper cleanup in
`finally`.
Test counts: 27/27 providerValidation (was 22, +5 xAI), 11/11
xaiOAuthCallback, 13/13 xaiOAuthShared. The repo guard now passes.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(xai): let Esc cancel xAI OAuth setup when manual-code input is focused
The manual-code TextInput's child-effect Esc handler ran before the
parent XaiOAuthSetup's `useKeybinding('confirm:no')`, so pressing Esc
triggered "press Esc again to clear input" instead of going back. Set
`disableEscapeDoublePress` so the parent keybinding fires immediately.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(xai): register Esc-to-back at ProviderManager top level
The child-component `useKeybinding('confirm:no', onBack)` in
XaiOAuthSetup wasn't reliably firing while the manual-code TextInput
held the input loop — even with disableEscapeDoublePress, the input's
listener still ran first and the keybinding context resolution lost
the race in practice. Move the binding to the top level of
ProviderManager with `context: 'Settings'` and `isActive: screen ===
'xai-oauth'`, matching the proven preset-api-key pattern (which also
has a TextInput and where Esc works correctly).
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(xai): persist OAuth startup profile with marker so logout cleans it up
`setActiveProviderProfile()` for an xAI OAuth profile (provider='xai',
no API key) was writing the startup file via the generic
`buildOpenAICompatibleStartupEnv` path — producing a plain
profile='openai' file with OPENAI_BASE_URL=https://api.x.ai/v1 and no
credential marker. Two downstream bugs:
- `clearPersistedXaiOAuthProfile()` only matches files with
profile='xai' + XAI_CREDENTIAL_SOURCE='oauth', so the logout
cleanup left this file untouched. Next non-interactive launch
(e.g. `openclaude -p`) re-applied the stale base URL with no
credential and hit the missing-XAI_API_KEY validation warning
even though the user had just logged out.
- Startup validation could not distinguish "OAuth profile, token
will be resolved at request time" from "user just forgot to set
XAI_API_KEY".
`buildStartupProfileFromActiveProfile()` now detects xAI OAuth
profiles (xai vendor + empty apiKey) and writes profile='xai' with
XAI_CREDENTIAL_SOURCE='oauth'. `buildLaunchEnv()`'s xai branch
preserves the marker so it lands in process.env at startup, where
the existing xai-credential validation kind accepts it without
needing XAI_API_KEY.
Regression test: setActiveProviderProfile for an OAuth profile must
write the marker, isPersistedXaiOAuthProfile must recognise it, and
clearPersistedXaiOAuthProfile must remove the file.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(xai): set Access-Control-Allow-Private-Network so browser auto-detect works
Chrome/Edge require Access-Control-Allow-Private-Network: true on the
preflight response when an HTTPS origin (auth.x.ai) fetches a
private-network address (127.0.0.1). Without it the preflight returns
2xx but the actual GET is silently blocked — our loopback never
receives the callback, the CLI promise never resolves, and the user
has to fall back to pasting the code even after a successful sign-in.
Mirror openclaw's CORS setup: static `Allow-Methods: GET, OPTIONS`,
default `Allow-Headers: content-type` when none requested, and the
private-network header on every trusted-origin response.
Regression-locked because the failure mode is silent: the test now
asserts that an OPTIONS preflight from auth.x.ai with
Access-Control-Request-Private-Network: true gets the matching
Access-Control-Allow-Private-Network: true header back.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(xai): update mainLoopModel when xAI OAuth profile is activated
After the OAuth completion handler activated the new xAI profile, it
never wrote the new model back to app state. The chat session kept
sending the previous provider's model name (e.g. kimi-k2.6) against
api.x.ai/v1, yielding 400 "Model not found: kimi-k2.6". Mirrors the
existing activateSelectedProvider / saveAndCloseProvider flows that
set mainLoopModel + clear mainLoopModelForSession.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(xai): pause stdin on cleanup + CLI logout clears startup profile
Two P2 review items from the xAI OAuth PR:
1. `listenForManualCode()` resumed stdin unconditionally and cleanup
only removed the data listener. A resumed stdin keeps a one-shot
CLI process alive even after sign-in succeeds — the user sees
`openclaude auth xai login` complete but the prompt never returns
until they send EOF. Now records the pre-existing paused state and
only resumes (and re-pauses on cleanup) when stdin was paused to
begin with.
2. `openclaude auth xai logout` only cleared secure storage. If the
user had configured xAI OAuth through `/provider`, the
marker-tagged `.openclaude-profile.json` and the provider profile
in global config both survived. Startup validation still accepted
`XAI_CREDENTIAL_SOURCE=oauth`, but openaiShim could no longer
resolve a token — the next non-interactive xAI launch was left
pointed at api.x.ai with no credentials instead of being logged
out cleanly. CLI logout now mirrors the /provider UI logout:
clear secure storage → delete the xAI OAuth provider profile from
global config (matched by canonical name) → remove the
marker-tagged startup file → clear the global startup-provider
override if the active profile changed.
New regression tests in `src/cli/handlers/xaiAuth.test.ts`:
- logout removes the marker-tagged startup profile (the documented
/provider-then-CLI-logout sequence)
- logout is a no-op when no profile is stored
- logout leaves unrelated (non-xAI) startup profiles alone
The tests assert file-level cleanup directly because Bun's
`mock.module(...)` in ProviderManager.test.tsx leaks providerProfiles
stubs across files within the same `bun test` process; in-memory
lookups aren't reliable here, but the startup-file path is what users
actually hit at next launch.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* test(xai): make xaiLogout regression test hermetic in the parallel suite
The test passed in isolated runs but failed in the full `bun test` run
(2812 pass / 1 fail). Two compounding leaks from other test files:
1. ProviderManager.test.tsx / model.test.tsx / others install
`mock.module('../utils/providerProfile.js', ...)` stubs that omit
`clearPersistedXaiOAuthProfile`. Bun's `mock.restore()` only
restores `mock.fn()` mocks; module mocks persist across files in
the same process, so xaiAuth's static import of
`clearPersistedXaiOAuthProfile` resolved to `undefined`.
2. SQLite / knowledgeGraph / paths tests call
`setClaudeConfigHomeDirForTesting(...)` in parallel and don't
always restore it, so the fresh providerProfile module's call to
`getClaudeConfigHomeDir()` returned a leaked override instead of
our CLAUDE_CONFIG_DIR. The real `clearPersistedXaiOAuthProfile`
ran fine, just against the wrong directory.
Refactor `xaiLogout` to accept an optional `XaiLogoutDeps` object so
the test can inject:
- the real `clearPersistedXaiOAuthProfile` (resolved via cache-bust
import) wrapped to pin `configDir: tempConfigDir`, bypassing the
parallel-test override leak
- the real `clearXaiCredentials` / `getProviderProfiles` /
`deleteProviderProfile` / `clearStartupProviderOverrides`,
bypassing the `mock.module` leak
Production callers (just `main.tsx`) omit the argument and pick up the
static imports — behavior unchanged.
Full test count: 2813 pass / 0 fail (was 2812 pass / 1 fail).
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(xai): isolate OAuth profile from shell OPENAI_API_KEY + close callback on early cancel
Two more review items from the xAI OAuth PR.
P1 — buildLaunchEnv() leaked OPENAI_API_KEY into xAI OAuth profiles.
For a marker-tagged xAI profile, the launch env builder fell back
through processEnv.OPENAI_API_KEY → persistedEnv.OPENAI_API_KEY when
no XAI_API_KEY was set, then handed that key to buildXaiProfileEnv()
which copied it to BOTH OPENAI_API_KEY and XAI_API_KEY. The OAuth
credential-source marker was then dropped (because env.XAI_API_KEY
became truthy), and openaiShim short-circuited on the ambient
OPENAI_API_KEY before ever resolving the stored OAuth token —
sending the user's generic OpenAI key as a bearer to api.x.ai/v1.
Fix: when the persisted profile is OAuth-tagged, build xaiKey only
from explicit XAI_API_KEY env (shell or persisted), never from
OPENAI_API_KEY. Pass a scrubbed processEnv into buildXaiProfileEnv()
so it can't re-introduce the key via its internal fallback. Also
clear OPENAI_API_KEY from the returned launch env (defensive: the
clearManagedProfileEnv step already drops it, but be explicit).
Three regression tests:
- ambient OPENAI_API_KEY does NOT leak into XAI_API_KEY /
OPENAI_API_KEY for an OAuth profile; XAI_CREDENTIAL_SOURCE
survives.
- explicit XAI_API_KEY still overrides OAuth (existing precedence
preserved).
- non-OAuth xAI profile (legacy api-key flow, no marker) still
accepts the OPENAI_API_KEY fallback — backward compat.
P2 — useXaiOAuthFlow leaked the loopback callback server when the
user cancelled mid-start. If unmount/Esc happened while
beginOAuthFlow() was still awaiting discovery or starting the
listener, the cleanup ran before the service had tracked the handle.
beginOAuthFlow() then resolved, the IIFE saw `cancelled === true`
and returned without closing the just-started server — leaving the
fixed 56121 port held. Next OAuth attempt failed with EADDRINUSE.
Fix: track the resolved handle in a closure variable shared between
the IIFE and the cleanup callback. The IIFE calls handle.cancel()
when it observes cancellation after beginOAuthFlow resolved; the
cleanup callback also calls activeHandle?.cancel() to cover the
common "handle exists by unmount" case. handle.cancel() → service
cleanup is idempotent.
New test file `useXaiOAuthFlow.test.tsx` with two cases:
- unmount while beginOAuthFlow is pending → cancel fires once
beginOAuthFlow eventually resolves
- unmount after handle exists → cancel fires immediately
Test count: 2818 pass / 0 fail (was 2813), TS error count unchanged
at 1692.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
---------
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
The gRPC entrypoint constructed QueryEngine with `agents: []`, so the
moment the model tried to spawn a subagent (e.g. `general-purpose` for
project investigation) the Agent tool threw "Agent type 'general-purpose'
not found. Available agents: " with nothing after the colon. The CLI
entrypoint hydrates these via getBuiltInAgents(); do the same here so
gRPC-hosted sessions (playground sandbox, etc.) get parity.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Opengateway flipped from zero-auth to per-user API keys (mint at
https://gitlawb.com/opengateway/keys). Update the client to match:
- Descriptor: setup.requiresAuth=true, authMode='api-key',
credentialEnvVars=['OPENGATEWAY_API_KEY','OPENAI_API_KEY'] in both
setup (used by the generator + auth-prompt) and validation (used by
getProviderValidationError). Added missingCredentialMessage pointing
users at the console URL.
- Transport: defaultAuthHeader changed from {name:'api-key',scheme:'raw'}
to {name:'authorization',scheme:'bearer'} — the gateway only validates
Authorization: Bearer ogw_live_..., the previous raw 'api-key' header
was a leftover from the direct-Xiaomi era.
- Auto-detect: defaultOpengatewayProvider now returns null when no key
env var is set instead of unconditionally selecting opengateway —
surfaces the missing-credential prompt instead of silently routing to
an endpoint that will 401.
- Tests: providerValidation.test.ts no-auth tests replaced with
positive/negative key cases; providerAutoDetect.test.ts updated four
fallback tests to include OPENGATEWAY_API_KEY (or assert null for the
empty-env case).
- Regenerated integrationArtifacts.generated.ts via integrations:generate.
Full test suite: 2783 pass / 0 fail.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue #603 reports that the terminal input freezes shortly after startup
when MCP plugins (especially HTTP/SSE servers with OAuth) are enabled.
`--bare` mode, which skips MCP initialization, works around the freeze.
Root cause analysis points to a race between:
1. Ink's `handleSetRawMode` setting up stdin listeners during REPL mount
2. Async MCP connection callbacks triggering React re-renders via `setAppState`
Rapid mount/unmount of `useInput` components during those re-renders can
unbalance `rawModeEnabledCount`, driving it negative. Once negative,
subsequent `setRawMode(true)` calls skip the stdin setup (because the
count is no longer `0`), leaving the terminal with no active input handler
and producing the "frozen" symptom.
Changes:
- **Ink App.tsx**: Guard `rawModeEnabledCount` against negative values on
both enable and disable paths. If the count is negative on enable, reset
it to `0` so the setup path runs. If the count is already `<= 0` on
disable, ignore the call instead of decrementing further.
- **useManageMCPConnections**: Increase `MCP_BATCH_FLUSH_MS` from `16` to
`100` to coalesce more MCP state updates into fewer React commits.
- **useManageMCPConnections**: Defer `loadAndConnectMcpConfigs()` by one
`setTimeout(..., 0)` tick so Ink's initial stdin raw-mode setup fully
commits before any MCP async callback can interleave with it.
Generated with [Devin](https://cli.devin.ai/docs)
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Shell-tool outputs (BashTool result `stderr`, ShellError `stdout`/`stderr`)
surface as null when the stream produced nothing. processBashCommand piped
those straight into escapeXml, and the previous `s.replace(...)` body
crashed the REPL session with:
TypeError: Cannot read properties of null (reading 'replace')
at escapeXml
at processBashCommand
Widen the parameter to `string | null | undefined` and short-circuit to `''`.
escapeXmlAttr delegates to escapeXml so it inherits the guard.
Closes#1247.
* fix: allow providers without OPENAI_API_KEY if auth is not required or specific key exists
- Added validation metadata to gitlawb-opengateway to indicate it doesn't require auth.
- Updated getProviderValidationError to respect requiresAuth: false and provider-specific credential env vars.
- This prevents mandatory OPENAI_API_KEY check from blocking other providers when an OpenAI profile is active.
* test: add regression test for opengateway no-auth validation path
Ensures getProviderValidationError returns null when OPENAI_BASE_URL
points at opengateway.gitlawb.com and OPENAI_API_KEY is absent.
* test: add model-specific path test for opengateway
* fix: remove unreachable providerValidation.ts changes per review
The descriptor metadata in gitlawb-opengateway.ts already handles the
no-auth validation. The additional check in providerValidation.ts was
unreachable code.
---------
Co-authored-by: professional-slacker <professional-slacker@users.noreply.github.com>
* Fix OpenGateway MiMo agent tool history
Stop the Gitlawb Opengateway preset from forcing synthetic empty reasoning_content onto prior assistant tool-call messages. MiMo rejects that shape after Agent/sub-agent tool calls with upstream Param Incorrect errors.
Add an OpenAI shim regression test covering MiMo tool history through https://opengateway.gitlawb.com/v1 while preserving the separate OpenGateway Gemini signature replay behavior.
Validation: bun test src/services/api/openaiShim.test.ts; bun run build
* fix(MiMo): update integration to remove unnecessary body fields and preserve reasoning content
* test(MiMo): enhance tests to verify reasoning_content handling and strip unsupported options
---------
Co-authored-by: jatmn <the@jat.mn>
* fix: allow providers without OPENAI_API_KEY if auth is not required or specific key exists
- Added validation metadata to gitlawb-opengateway to indicate it doesn't require auth.
- Updated getProviderValidationError to respect requiresAuth: false and provider-specific credential env vars.
- This prevents mandatory OPENAI_API_KEY check from blocking other providers when an OpenAI profile is active.
* test: add regression test for opengateway no-auth validation path
Ensures getProviderValidationError returns null when OPENAI_BASE_URL
points at opengateway.gitlawb.com and OPENAI_API_KEY is absent.
* test: add model-specific path test for opengateway
* fix: add 5-minute timeout to QueryGuard to prevent infinite spinner loops
When an API call hangs or the response-received state transition fails,
the spinner runs indefinitely consuming memory (observed at 4GB+ after
24h). This adds a watchdog timer that force-ends the query after 5
minutes, resetting the spinner to idle.
Changes:
- QueryGuard.tryStart() now starts a 5-minute watchdog timer
- QueryGuard.end() and forceEnd() clear the timer
- If timeout fires, forceEnd() is called and a console.error is logged
- Added unit tests for timeout behavior
---------
Co-authored-by: professional-slacker <professional-slacker@users.noreply.github.com>
Buffer incomplete UTF-8 byte sequences across stdin parser reads so interactive IME input does not turn split multibyte characters into replacement text.
Keep the existing high-bit meta-key fallback when a lone pending byte is flushed, and continue to route completed text through the existing terminal tokenizer.
Add regression coverage for Vietnamese input arriving one byte at a time, matching the live typing failure mode from issue #1233.
Validation: bun test src\ink\parse-keypress.test.ts; bun test src\components\TextInput.test.tsx src\ink\parse-keypress.test.ts; bun run build; git diff --check.
The overflowX="hidden" added in #1211 clips task subject text to
nothing when TaskItems are nested inside MessageResponse (the └
prefix constrains available width). The icon survives at 2 chars
but the text gets fully clipped, leaving orphaned ✓/■ without
any label.
Reverts the overflowX="hidden" portion of #1211.
* fix(bashPermissions): apply MAX_SUBCOMMANDS cap in sandbox auto-allow path (#1057)
The cap from #21405 is enforced in `bashToolHasPermission`, but the
`checkSandboxAutoAllow` shortcut path called `splitCommand` and iterated
`matchingRulesForInput` once per subcommand before the main-path cap got
a chance to run. With auto-allow-bash-if-sandboxed enabled, a crafted
compound command whose legacy `splitCommand` output explodes could
trigger N rule lookups in this path.
Mirror the existing cap (MAX_SUBCOMMANDS_FOR_SECURITY_CHECK = 50) in
`checkSandboxAutoAllow` right after the `splitCommand(command)` call:
log + return `ask` with the same decision-reason shape as the main path.
Regression test in bashPermissions.test.ts exercises sandbox auto-allow
with 60 echo subcommands and asserts the `ask` short-circuit.
* ci: re-trigger checks (likely-flaky failures on unrelated profile/SDK/KG tests)
* fix(bashPermissions): gate sandbox cap on legacy splitter path (CC-643)
Address reviewer feedback on #1057: the previous patch applied
MAX_SUBCOMMANDS_FOR_SECURITY_CHECK in checkSandboxAutoAllow
unconditionally on splitCommand output. The main bashToolHasPermission
path only applies that cap when astSubcommands === null, because the
fanout/ReDoS concern is specific to the legacy splitter — AST-parseable
compound commands (e.g. long echo chains) are already bounded by
structural parse and should not be downgraded to ask.
Thread astSubcommands into checkSandboxAutoAllow and only fire the cap
when AST is unavailable. Export checkSandboxAutoAllow so the symmetric
behavior is directly testable without depending on tree-sitter WASM
availability in the test runtime.
Tests:
- Legacy path (CLAUDE_CODE_DISABLE_COMMAND_INJECTION_CHECK=1 OR
astSubcommands=null) over 50 subcommands -> ask, cap reason.
- AST-validated path (astSubcommands provided) with 60 subcommands ->
allow, sandbox auto-allow reason.
Give acquireSharedMutationLock a default five-minute timeout so missed releases fail with a scoped error instead of hanging the smoke suite indefinitely.
Keep explicit timeout overrides intact and add isolated mutex coverage for default timeout, override timeout, and release handoff behavior.
The spinner row used flexWrap="wrap" which caused the status text
(thinking indicator, timer, token count) to wrap to a new line when
content width hit a boundary condition. This produced a visible
layout jump — especially during thinking transitions when the status
text changes width.
Additionally, TaskItem rendered icons without overflow constraints,
so when text content overflowed the available width, orphaned icon
characters (checkmarks, squares) leaked into visible rows.
Changes:
- Use flexWrap="nowrap" on spinner row containers to keep status on
one line, relying on the existing progressive width gating to hide
elements that don't fit
- Replace magic number in availableSpace calculation with a named
constant for clarity
- Add overflowX="hidden" on TaskItem to clip overflowing content