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
* fix(websearch): surface adapter failure when auto mode falls back to native (#994)
When `WEB_SEARCH_PROVIDER=auto` and the configured adapter chain fails
on a recoverable error (DuckDuckGo "rate-limited from this network",
adapter timeout, 5xx, etc.), the tool falls through to the native
Anthropic / Codex web-search path silently. The only signal that the
adapter failed is a `console.error` line — it never reaches the tool
result the user sees. On rate-limit-prone networks (datacenter IPs,
VPNs) this manifests as "no results found" with no actionable hint,
exactly the symptom reported in #994.
This change captures the adapter error in `adapterFallthroughNotice`
inside the catch branch and prepends it to the eventual native / Codex
output via a small pure helper, `withAdapterFallthroughNotice`. The
hits-present and native-error paths are unchanged; the helper only
mutates a shallow copy when a notice is set, and is a no-op otherwise.
Result: users on a rate-limited adapter chain who get native results
also see *why* the adapter failed, and users whose native search also
returns nothing finally get the actionable diagnostic (configure
TAVILY_API_KEY / FIRECRAWL_API_KEY / etc.) instead of a silent empty.
Test coverage in WebSearchTool.test.ts asserts the pure-helper
contract: no-op when notice is undefined, prepend-not-mutate when a
notice is provided.
* fix(websearch): narrow #994 fix to the reachable adapter-failure surface
Address @techbrewboss feedback: the previous patch's
`adapterFallthroughNotice` machinery and the
`withAdapterFallthroughNotice` helper were unreachable under the
current provider selection.
`shouldUseAdapterProvider()` and `hasNativeSearchFallback()` are
mutually exclusive in auto mode — when a native path exists
(firstParty/vertex/foundry/Codex) the adapter is never tried, and when
the adapter IS tried (openai-shim providers) there is no native
fallback. So the assignment at `adapterFallthroughNotice = ...` and
both `withAdapterFallthroughNotice(...)` call sites could never fire.
Narrow the PR to the path that #994 actually hits today: an
openai-shim provider (moonshot/minimax/nvidia-nim/github copilot) where
the adapter fails transiently and there is no native fallback. The
existing throw at that branch already surfaces the underlying adapter
error verbatim; extract `buildAdapterUnavailableError(provider, errMsg)`
so it is directly testable and cannot regress, and replace the dead
notice helper + its tests with focused coverage of the reachable
message.
Drop the no-op shallow-copy `withAdapterFallthroughNotice` helper and
its two tests; keep the descriptive error throw as the single,
reachable surfacing path.
Closes#402 — JavaScript heap OOM during large tasks.
The CLI entry point only set --max-old-space-size=8192 when
CLAUDE_CODE_REMOTE=true, leaving local users with V8's ~2 GB default
ceiling. Long agentic tasks (multi-file refactors, large prompts, tool
loops) hit that ceiling and abort with:
FATAL ERROR: Ineffective mark-compacts near heap limit
Allocation failed - JavaScript heap out of memory
Fix: remove the CCR gate and apply the 8 GB cap unconditionally, with a
user-override guard -- if the runner already set NODE_OPTIONS
--max-old-space-size to an explicit value, their setting is preserved
(no silent clobbering).
Files changed:
- src/entrypoints/cli.tsx — remove CLAUDE_CODE_REMOTE guard, add
user-override predicate, update comments
- src/entrypoints/cli.test.ts — 6 regression tests (new file)
* feat(opengateway): add Gemini 3.1 Flash Lite + GLM 5.1 FP8 to catalog
Opengateway now routes non-Xiaomi models through GMI Cloud (configured
in opengateway/src/providers.ts via modelIds:
- google/gemini-3.1-flash-lite-preview
- zai-org/GLM-5.1-FP8
Adding both as catalog entries on the gitlawb-opengateway gateway
descriptor so openclaude users see them in the model picker when the
Opengateway preset is active. Each catalog entry reuses the existing
upstream model descriptor (`gemini-3.1-flash-lite-preview`, `GLM-5.1`)
for capability metadata; the apiName uses the full vendor-prefixed
form the gateway routes on.
No new model/brand/vendor descriptors needed — only the gateway
catalog gets the new IDs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* update opengateway
---------
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix flaky smoke build checks
Replace feature-flag build preprocessing with a Bun onLoad transform so smoke/build no longer rewrites tracked src files while tests may be reading them.
Keep telemetry stubs ahead of the feature transform in both CLI and SDK builds, preserve non-empty text token counts in hybrid context splitting, and make the corrupted Orama stress test assert against the actual project directory used by the test.
Verified with bun run smoke, bun test src/utils/hybridContextStrategy.test.ts, bun test src/utils/knowledgeGraph.stress.test.ts --rerun-each 3, and bun test --max-concurrency=1.
* Harden KnowledgeGraph smoke stress isolation
Give each KnowledgeGraph stress test its own temporary config directory and remove it during teardown so Orama, SQLite, and corrupted-file state cannot bleed between stress cases or later PR test runs.
Reviewed at least 20 open PRs and found the recurring smoke-and-tests failure cluster is the full unit suite, especially KnowledgeGraph corrupted Orama recovery. Verified with bun test src/utils/knowledgeGraph.stress.test.ts --rerun-each 5, bun test --max-concurrency=1, and bun run smoke.
* Harden smoke test isolation
Audit and harden broad smoke-adjacent test suites for process-global leaks, including env/config restoration, shared registry/module mock cleanup, fetch/axios/mock restoration, and global MACRO/platform/sandbox mutations.
Replace fragile render sleeps in interactive tests with output-driven waits, and isolate provider/model/profile tests behind the shared mutation lock so unrelated PRs do not inherit stale process state.
Make SQLite knowledge graph cleanup clear closed on-disk databases before best-effort file cleanup, with coverage for the stale database reset path.
Verified: bun run smoke; bun test --max-concurrency=1; python -m pytest -q python/tests; bun run security:pr-scan -- --base origin/main; bun run test:provider; npm run test:provider-recommendation.
* Harden test isolation across smoke suite
Guard process-global test mutations with the shared mutation lock across env, module mock, config cache, and storage tests.\n\nDeep-copy global config snapshots, restore transient globals precisely, and make plugin/LSP mocks expose compatible export surfaces so concurrent test loading does not poison unrelated suites.\n\nReplace fixed SDK cleanup sleeps with call polling to remove timing sensitivity.\n\nVerification:\n- bun test --max-concurrency=1\n- bun run smoke\n- python -m pytest -q python/tests\n- bun run test:provider\n- npm run test:provider-recommendation\n- bun run security:pr-scan -- --base origin/main\n- git diff --check
* Close remaining test global-state leaks
Guard remaining cache, plugin, console, and VS Code module-mock tests with the shared mutation lock.\n\nThis follow-up audit covers non-env process-global state that can leak across test files: tool schema cache, cache stats tracker state, plugin loader caches, console.error replacement, and VS Code mock.module usage.\n\nVerification:\n- leak-surface scans for env/global/mock.module/cache outliers\n- duplicate top-level mock collision cluster\n- affected tests cluster\n- bun test --max-concurrency=1\n- bun run smoke\n- git diff --check
* Guard remaining mock restore cleanup
Lock tests that call bun:test mock.restore without installing module mocks themselves.\n\nmock.restore is process-global, so these cleanup hooks can still tear down another test file's active module mocks when files run concurrently.\n\nVerification:\n- expanded leak scans for env, globals, module mocks, mock.restore, timers, argv, and caches\n- bun test src/components/useCodexOAuthFlow.test.tsx src/services/github/deviceFlow.test.ts\n- bun test --max-concurrency=1\n- bun run smoke\n- git diff --check
* Harden test isolation for smoke stability
Serialize tests that mutate process-global state behind the shared mutation lock, including process.env, transient globals, global config/cache state, storage mocks, and Bun module mocks.
Add isolated env mutex instances for SDK mutex tests so timeout coverage no longer manipulates the live process-global mutex.
Move top-level mock.module setup behind lock acquisition and restore mocks before releasing locks to prevent cross-file leakage under parallel smoke runs.
Verified with: bun test --max-concurrency=1; bun test; bun run smoke.
* chore: centralize Bun version and refresh CI tool pins
- add .bun-version as the shared Bun source of truth for workflows and Docker builds
- update PR and release workflows to read Bun from bun-version-file
- refresh pinned GitHub Actions and Docker action SHAs to newer low-risk releases
- align contributor docs with Bun 1.3.13 guidance
* test: stabilize reset and provider profile persistence
Harden knowledge graph reset behavior across Windows file-lock scenarios by improving SQLite and JSON reset signaling, preserving a safe JSON source of truth when SQLite cannot be cleared, and adding direct storage regression coverage.
Also centralize deterministic config-home handling for tests, tighten provider profile persistence path resolution and cleanup semantics, isolate environment-sensitive suites with the env mutex, and remove flaky external npx dependency from the SDK consumer type test.
* test: fix Codex OAuth callback flake
Investigate the real provider smoke failure from GitHub Actions and fix the root cause instead of patching the symptom.
- make Codex OAuth callback host explicit and consistent across redirect URI generation and listener binding
- allow safe loopback host overrides for localhost, 127.0.0.1, and ::1
- harden Codex OAuth tests with env/fetch isolation so they do not poison neighboring provider suites
- pin the OAuth callback tests to 127.0.0.1 to avoid localhost IPv4/IPv6 family mismatch flakes in CI
Validated with bun test src/services/api/codexOAuth.test.ts, bun test src/services/api/providerConfig.codexSecureStorage.test.ts, and bun run test:provider.
* test: harden Codex OAuth callback tests
Investigate the recurring provider-smoke OAuth failures across multiple PR runs and fix the flaky callback test design at the root.
- remove the free-port reservation race from Codex OAuth tests
- add bounded callback retry only for loopback listener warm-up during the in-process OAuth test flow
- move ephemeral callback port support into an explicit CodexOAuthService test seam instead of widening production env parsing
- keep runtime callback-port semantics unchanged while adding regression coverage for callback host and port parsing
Validated with targeted Codex OAuth tests and repeated provider-bucket reruns to check for recurring flake.
* test: serialize provider shared-state suites
Fix the recurring provider smoke flake at the root cause by serializing test suites that mutate process.env or globalThis.fetch.
Add a shared test mutation lock and wire it into the provider bucket so Codex OAuth no longer races with unrelated provider/config/openai shim tests under Bun's parallel test execution. Cleanup now releases the lock in finally blocks, and the shared lock waits indefinitely by default to avoid timeout-based CI flakes.
* test: fix smoke root causes and noisy suites
Replace the Codex OAuth test's live loopback listener dependency with an injected listener seam, avoid module-mock leakage across provider suites, and clean up the auth-code listener test setup.
Also harden noisy storage and search tests by asserting expected log output, isolating SQLite masterpiece persistence per test cwd, and removing routine benchmark/stress logging from passing runs.
* build: harden Bun version install in Docker
Validate the repo-tracked .bun-version value before using it in the Docker build stage, strip line endings, and install Bun through a quoted semver-only variable instead of raw shell expansion.
* test: replace flaky conversation arc benchmark
Fix the recurring smoke failure caused by an absolute wall-clock assertion in the normal unit suite. Replace the CI-speed-sensitive conversation arc benchmark with deterministic regression coverage that verifies repeated fact extraction, expected entity shapes, bounded graph growth, and populated-summary behavior.
* test: isolate shared-state smoke suites
* test: restore codex credential mocks between suites
* test: fix shared-state and provider init-order flakes
* test: isolate remaining shared-state smoke suites
Serialize the remaining smoke-sensitive suites that mutate process env, CLAUDE_CONFIG_DIR, fetch, or SDK session globals.
Add shared lock coverage to discovery, agent/skills loading, platform storage, and SDK lifecycle/preserved-segment tests. Restore session and cwd state inside the lock boundary so parallel files cannot leak bootstrap state into knowledge graph and SDK isolation tests.
Validated with repeated smoke and full-suite passes:
- bun run smoke (2x)
- bun test
- bun test --max-concurrency=1
- bun run test:provider
- python -m pytest -q python/tests
- npm run test:provider-recommendation
Two related fixes to the OpenAI-compatible streaming parser:
1. Detect `data: {"error": {...}}` events inside the stream and throw a
proper APIError. OpenAI sends this when a stream fails after headers
have been sent, and intermediaries (gateways, proxies) use it to
signal structured failures without dropping the TCP connection.
Previously this chunk was silently dropped and the parser kept
waiting for [DONE], producing the confusing "unexpected response"
error after the stream ended without a proper close.
2. When finish_reason is "length" (model hit max_tokens, OR an upstream
watchdog synthesized a graceful end after detecting a stalled
stream), append a visible "[Response truncated]" hint inline. Mirrors
the existing content_filter hint pattern. Users now know to ask the
model to continue rather than wondering why the answer cut off.
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(provider): add Gitlawb Opengateway as default provider with MiMo
Registers https://opengateway.gitlawb.com/v1/xiaomi-mimo as a first-class
gateway descriptor in openclaude. Pins it first in the provider picker
order (ahead of anthropic) and surfaces it with a green [FREE] badge in
the picker UI. Wires detectBestProvider() to fall back to opengateway
with mimo-v2.5-pro when no other credentials or local services are
detected, making fresh installs work zero-config during the Xiaomi
free-inference partnership window.
- src/integrations/gateways/gitlawb-opengateway.ts: gateway descriptor,
static catalog of all five mimo models, requiresAuth: false
- src/integrations/artifactGenerator.ts: sort comparator pins
gitlawb-opengateway before anthropic
- src/utils/providerAutoDetect.ts: opengateway is the last-resort
fallback; OPENGATEWAY_BASE_URL env override for local dev;
skipOpengatewayFallback escape hatch for tests
- src/components/ProviderManager.tsx: getPresetLabel renders a green
[FREE] badge for the opengateway preset, matching the existing
[Sponsor] badge pattern used for xiaomi-mimo
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(provider): keep Codex OAuth after DeepSeek with Opengateway pinned
Pinning Gitlawb Opengateway at the top of the preset list shifted every
subsequent index by 1, which dropped Codex OAuth from "after DeepSeek"
to "before DeepSeek" because the splice insertion index was a hardcoded
6. Bumps the insertion index to 7 to keep Codex OAuth in its established
position, and updates PRESET_ORDER in ProviderManager.test.tsx so the
keyboard-navigation harness lines up with the new layout.
Restores 9 ProviderManager tests that were timing out because
navigateToPreset() was landing on the wrong row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Added 'Don't take over' rule to AgentTool prompt to prevent models from overriding forks based on partial output.
- Refined 'Don't peek' rule to explicitly direct models to use SendMessage for course-correction instead of Read.
- Clarified that override decisions belong to the Review phase.
The `ollama launch openclaude` syntax in README and advanced-setup.md
was docs-only (added in #716), gated on the companion
ollama/ollama#15618 integration that has not landed upstream. Users
following these instructions get `Error: unknown integration:
openclaude` (issue #744 originally, #1134 now).
Remove the misleading section + table mention so the env-var setup
above remains the documented path. Can be re-added once the upstream
ollama integration ships.
Closes#1134
Prevent the legacy .openclaude-profile.json fallback from overriding startup env when a modern configured provider profile has already selected a concrete provider configuration.
Thread the configured-profile signal from CLI bootstrap into buildStartupEnvFromProfile(), add a concrete-selection helper for the new guard, and preserve the legacy file as a first-run fallback when startup env is incomplete.
Also fix the follow-up falsey-flag regression so disabled CLAUDE_CODE_USE_* values do not count as active startup selections, and add regression tests covering stale legacy overrides, incomplete startup env, and falsey provider flags.
Verified with: bun test src/utils/providerProfile.test.ts --test-name-pattern " buildStartupEnvFromProfile\
* feat(provider): add Xiaomi MiMo integration
Add Xiaomi MiMo as an official OpenAI-compatible provider with provider-profile persistence, env-only detection, and sponsor labeling in the provider picker.
Co-Authored-By: OpenClaude (zai-org-glm-5-1) <openclaude@gitlawb.com>
* feat(provider): rebase Xiaomi MiMo as top-level provider
Promote Xiaomi MiMo from generic OpenAI-compatible shim route to a
first-class top-level provider matching the MiniMax pattern. Includes
brand/model descriptors, env-only detection, provider auto-detect,
status labels, legacy provider type, model picker, and profile env
persistence.
Co-Authored-By: OpenClaude (zai-org-glm-5-1) <openclaude@gitlawb.com>
* Fix Xiaomi MiMo provider integration
Promote Xiaomi MiMo as a first-tier OpenAI-compatible vendor and align its descriptor with the integration guide.
Use the resolving Xiaomi MiMo API host while normalizing the stale docs host alias, wire Xiaomi catalog options into /model, and ensure model selection/display uses OPENAI_MODEL for Xiaomi instead of Claude defaults.
Update provider profile/startup handling, OpenAI shim detection, docs, generated integration artifacts, and focused regression tests.
Verification: bun run integrations:check; focused bun test provider/model suites; bun run build; bun run smoke.
* Fix MiMo startup profile base URL normalization
---------
Co-authored-by: OpenClaude (zai-org-glm-5-1) <openclaude@gitlawb.com>
Co-authored-by: JATMN <the@jat.mn>
The gpt-5.5 model descriptor was set to the OpenAI API ceiling of
1,050,000 tokens, but in this repo gpt-5.5 is primarily routed through
the Codex transport (src/services/api/providerConfig.ts), whose practical
request limit is ~272k. The mismatch caused /context to under-report
usage and auto-compact to fire after the request had already exceeded the
effective window, surfacing as a mid-turn 500 'input exceeds the context
window of this model'.
Pin the descriptor to the Codex limit so /context shows realistic
budgets and auto-compact triggers before the failure. Provider-aware
context windows via the existing route catalog system remain the
correct long-term fix; this is the conservative interim.
Fixes#1118