Commit Graph
670 Commits
Author SHA1 Message Date
0xfandom 01ffbb68b8 fix(provider): allow remote Ollama without OPENAI_API_KEY (#952)
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
2026-05-27 08:36:20 +08:00
Neodiusanddaltoncoder 363583faf5 fix(launcher): route direct Node launch paths through launcher (#1363)
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>
2026-05-27 08:19:27 +08:00
chioarub 8513178934 fix(thinking): disable thinking for unsupported Ollama models (#1376)
* 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
2026-05-27 07:55:25 +08:00
github-actions[bot] 670744fc70 chore(main): release 0.15.0 (#1325)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
v0.15.0
2026-05-26 22:11:32 +08:00
chioarub f15d2360ee fix(codex): allow credential storage fallback (#1347)
* fix(codex): allow credential storage fallback

* fix(codex): scope plaintext credential fallback
2026-05-26 21:54:33 +08:00
chioarub 6bc050e621 fix(attribution): make git attribution opt-in by default (#1335) 2026-05-26 21:53:05 +08:00
chioarub 785d3de2cd fix(agent): allow custom model overrides (#1337)
* fix(agent): allow custom model overrides

* fix(agent): enforce model allowlist for overrides

* test(agent): isolate allowlist override regression tests

* fix(agent): validate teammate model overrides
2026-05-26 21:36:47 +08:00
Vasanth TandOpenClaude Worker 3 4ee279e7de ci: retrigger CodeQL after action download outage (#1374)
Co-authored-by: OpenClaude Worker 3 <worker-3@openclaude.local>
2026-05-26 21:31:46 +08:00
chioarub 7419d3800c feat(agents): set active session agent from agents menu (#1349)
* feat(agents): set active session agent from agents menu

* fix agents menu model switching
2026-05-26 20:16:13 +08:00
JATMN 2c87bfe055 fix(model): include profile models in descriptor picker (#1361)
* fix(model): include profile models in descriptor picker

* fix(model): respect active profile model lists
2026-05-26 20:14:47 +08:00
chioarub ed91673f53 fix(watchers): debounce skills and settings reload bursts (#1370) 2026-05-26 20:13:55 +08:00
3kin0x 2f8aa50cf6 feat(query): robust multi-lingual and structural continuation nudge (#1280)
* feat(cli): improve SSH interactivity detection via SSH_TTY and SSH_CONNECTION

* feat(models): add support for Gemma 4 31B

* feat(query): robust multi-lingual and structural continuation nudge

* fix(query): refine continuation nudge logic to avoid false positives
2026-05-25 19:33:13 +08:00
0xfandom 2d26a4673a fix(codex-stream): recover tool args delivered only via done events (#1262)
`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
2026-05-25 19:28:44 +08:00
JATMNandJATMN d02c10b399 feat: configure API retry backoff (#370) (#1095)
* 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>
2026-05-25 19:27:07 +08:00
JATMN b3dc674dbe fix: route MiniMax compacting through Anthropic-compatible API (#1154)
* 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.
2026-05-25 19:17:45 +08:00
JATMN cb666c85d0 Fix launcher heap setup for long sessions (#1242)
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.
2026-05-25 19:15:55 +08:00
0xfandom 4e8fa24cce feat(safety): warn at startup when 3P provider + permissive mode skip the AI classifier (#1260)
* 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.
2026-05-23 23:31:16 +08:00
0xfandom 07d9b4fec4 fix(json-schema): support top-level non-object roots via wrap/unwrap (#1261)
`--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
2026-05-23 23:23:09 +08:00
github-actions[bot] 66ed9b61dc chore(main): release 0.14.0 (#1217)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
v0.14.0
2026-05-23 13:16:56 +08:00
chioarub 0aff8de24f feat(diagnostics): show request payload size breakdown (#1237)
* feat(diagnostics): show request payload size breakdown

* fix(diagnostics): clarify request-size estimate semantics
2026-05-23 13:03:12 +08:00
0xfandom a44a83f38c fix(bash): preserve captured stdout in error message on non-zero exit (#1236)
* 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.
2026-05-23 13:02:14 +08:00
0xfandom 0d3c157149 fix(recovery): keep thinking blocks on resume for reasoning-echo providers (#1248)
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.
2026-05-23 12:58:26 +08:00
stamsam 8cd998de68 docs(windows): add npm global path fix (#1272) 2026-05-23 12:57:43 +08:00
stamsam 9b3c90418b fix(compact): clear native tool results after time compaction (#1278) 2026-05-23 12:55:54 +08:00
0xfandom 892c0545ed fix(retry): adjust max_tokens on OpenRouter 402 credit shortfall (#1263)
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
2026-05-23 08:50:04 +08:00
JATMN bafc2a1bc5 fix: harden XAA OAuth callback state handling (#1299)
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.
2026-05-23 08:49:05 +08:00
blouflabandblouf 9949ba7e24 Fix : Function call is missing a thought_signature in functionCall parts (#1302)
Co-authored-by: blouf <blouf@blouf.org>
2026-05-23 08:29:20 +08:00
Kevin CodexandOpenClaude 326f082682 feat(xai): add xAI/Grok OAuth provider (browser + device-code) (#1284)
* 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>
2026-05-23 08:22:57 +08:00
Kevin CodexandClaude Opus 4.7 c366f7062b fix(grpc): register built-in agents so Agent tool isn't always empty (#1296)
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>
2026-05-23 07:51:39 +08:00
Kevin CodexandClaude Opus 4.7 0fbfc12a99 feat(opengateway): require API key on /v1/* and switch to bearer auth (#1322)
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>
2026-05-23 07:49:20 +08:00
zucchiniandDevin 90360d3e38 fix(stdin,mcp): guard rawModeEnabledCount and defer MCP connections to prevent input freeze (#603) (#1268)
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>
2026-05-21 00:26:14 +08:00
Nishant Sarang eca9dba399 fix: treat blank Read.pages as omitted (#1269)
Fixes Gitlawb/openclaude#1264
2026-05-21 00:18:44 +08:00
0xfandom 03f879158c fix(xml): guard escapeXml/escapeXmlAttr against null and undefined (#1250)
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.
2026-05-20 02:31:42 +08:00
mee-kunandprofessional-slacker 4897d597b6 fix: allow non-OpenAI providers to skip OPENAI_API_KEY check (#1207)
* 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>
2026-05-20 02:22:01 +08:00
Meetpatel006andjatmn aab2fbcd7b fix: MiMo remove unsupported body fields and preserve reasoning content (#1253)
* 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>
2026-05-20 01:40:00 +08:00
mee-kunandprofessional-slacker 23254c21fb fix: add 5-minute timeout to QueryGuard to prevent infinite spinner (#1255)
* 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>
2026-05-20 01:38:35 +08:00
chioarub 1aa8aab84c fix(monitor): close permission dialog after selection (#1225)
* Fix monitor permission dialog lifecycle

* test(monitor): cover persistent allow continuation path
2026-05-20 01:37:09 +08:00
JATMN a9f8642aa8 fix(input): preserve split utf8 keypresses (#1241)
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.
2026-05-20 01:36:21 +08:00
chioarub f71e769237 fix(query): stop repeated tool-failure loops (#1219) 2026-05-18 07:23:36 +08:00
Evert Junior 0fba1541a8 fix(TaskListV2): revert overflowX hidden that hides task text labels (#1215)
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.
2026-05-17 14:55:00 +08:00
github-actions[bot] f102b601c5 chore(main): release 0.13.0 (#1208)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
v0.13.0
2026-05-17 11:13:06 +08:00
Kevin Codex e5535577aa docs: add Xiaomi MiMo sponsor (#1213)
* docs: add Xiaomi MiMo sponsor

* feat(tips): add Xiaomi MiMo sponsored tips
2026-05-17 11:01:06 +08:00
Kevin Codex 2d20109edc fix(gemini): parse raw tool call text (#1212) 2026-05-17 10:13:28 +08:00
Nik c53ef18716 fix(bashPermissions): apply MAX_SUBCOMMANDS cap in sandbox auto-allow path (#1057) (#1166)
* 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.
2026-05-17 09:55:27 +08:00
chioarub 271bad4209 feat(export): add Markdown and JSON conversation exports (#1193) 2026-05-17 09:54:33 +08:00
JATMN a7f4b02db9 test: add shared mutation lock timeout guard (#1209)
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.
2026-05-17 09:46:07 +08:00
Evert Junior 8470832e5c fix(spinner): prevent layout shift during thinking and orphaned task icons (#1211)
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
2026-05-17 09:45:23 +08:00
Nik b3b771476d fix(websearch): surface adapter failure when auto mode falls back to native (#994) (#1168)
* 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.
2026-05-17 05:35:28 +08:00
github-actions[bot] ca357cc78d chore(main): release 0.12.1 (#1202)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
v0.12.1
2026-05-17 05:31:30 +08:00
3kin0x bd7d42cb69 fix(ui): prevent prompt layout corruption when renaming session (#1206) 2026-05-17 05:31:10 +08:00