959 Commits
Author SHA1 Message Date
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
64955db0fb chore(main): release 0.21.0 (#1783)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
v0.21.0
2026-06-30 21:28:13 +08:00
BogdanandGitHub c1a9dadea5 fix(claude): make stream watchdog deterministic (#1823)
* fix(claude): make stream watchdog deterministic

* test(claude): harden watchdog env restore
2026-06-30 20:17:09 +08:00
0xfandomandGitHub a7945e5a70 fix(plugins): keep marketplace reconciliation prototype-safe (#1821)
Marketplace names come from user settings (extraKnownMarketplaces), and
diffMarketplaces looks each one up with materialized[name]. When the
materialized map is a plain object, a name colliding with an
Object.prototype member (constructor / toString / valueOf / hasOwnProperty)
resolves to the inherited value instead of undefined, so the entry is
misclassified as already-materialized (sourceChanged) rather than missing.

reconcileMarketplaces produced exactly such a plain object in its error
fallback. Two changes:

- diffMarketplaces now does an own-property-exact lookup via Object.hasOwn,
  so the comparison is correct regardless of the map's prototype.
- reconcileMarketplaces uses loadKnownMarketplacesConfigSafe, which already
  degrades a corrupted/unreadable config to a null-prototype empty map (and
  logs via logForDebugging rather than the error file). The snapshot is only
  used to diff — the install step re-reads and mutates the real config — so
  the graceful empty fallback cannot clobber the user's file.

Same prototype-pollution class as the marketplace cache fix in #1787.
Adds the first tests for diffMarketplaces, which was previously untested.
2026-06-30 20:16:05 +08:00
BogdanandGitHub 1827d84709 feat(agents): add per-agent step limits (#1815)
* feat(agents): add per-agent step limits

Add maxSteps agent configuration for markdown, JSON, plugin, and SDK agent definitions. Enforce the limit in subagent query execution by blocking over-limit tool calls, preserving a no-tool summary turn, and recording an agent_step_limit terminal reason. Add focused coverage for default behavior, invalid values, multi-turn accumulation, plugin parsing, failure-loop interaction, and summary-tool blocking.

* test(agents): isolate agent loader fixtures

* test(agents): stabilize agent loader config fixtures

* fix(agents): harden step-limit summaries

* fix(sdk): harden agent injection follow-up

* fix(sdk): report invalid agent step limits
2026-06-30 11:23:21 +08:00
JATMNandGitHub 985984b9ff feat(ClinePass): add gateway provider with usage support (#1818)
* feat(integrations): add ClinePass gateway provider with usage support

Adds ClinePass (https://cline.bot) as an OpenAI-compatible gateway provider.

Gateway
- New descriptor at src/integrations/gateways/clinepass.ts with 10 static models.
- Uses wireFormat: 'reasoning_effort' and full granular levels (low/medium/high/xhigh)
  so each model can expose reasoning controls consistent with Atlas Cloud.
- Dedicated credentials only: requires CLINE_API_KEY and ignores stale OPENAI_API_KEY.
- Generated integration artifacts updated via bun run integrations:generate.

/usage support
- New service module under src/services/api/clinepassUsage/ for types, fetching,
  and normalizing the ClinePass usage-limits response.
- New UI component src/components/Settings/ClinePassUsage.tsx rendered by Usage.tsx.
- Displays 5-hour, weekly, and monthly usage progress bars with longer progress bars.

Provider profile fixes
- routeMetadata.ts now resolves the active provider from the saved profile even when
  CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED is not yet set, so /usage works immediately
  after switching providers with /provider.
- providerProfile.ts / providerProfiles.ts learn CLINE_API_KEY so saved profiles apply
  the ClinePass credential alongside the OpenAI-compatible env vars.

Tests & docs
- Updated ProviderManager.test.tsx and compatibility.test.ts for the new preset.
- Added routeMetadata.test.ts cases for ClinePass and generic profile fallback.
- Added clinepassUsage.test.ts for payload normalization and row building.
- Updated .env.example and README.md with CLINE_API_KEY instructions.

Validation
- bun run typecheck
- bun run build
- bun run test:provider (964 pass)
- bun run check (5331 pass; 1 unrelated Windows file-mode failure in branch.test.ts)

* fixup: wire CLINE_API_KEY/CLINE_API_MODEL into runtime routing and profile persistence

- Add 'clinepass' to the ProviderProfile union so saved profiles can use it.
- Add CLINE_API_KEY to PROFILE_ENV_KEYS so profile switching clears stale keys.
- routeMetadata.ts: env-only CLINE_API_KEY now selects the clinepass route; other
  env-only intents treat CLINE_API_KEY as a competing credential. Added
  isClinePassBaseUrl/getClinePassBaseUrlOverride and activeProfileBaseUrl option
  to resolveActiveRouteIdFromEnv so custom/unknown profiles targeting api.cline.bot
  resolve correctly.
- providerConfig.ts: resolveProviderRequest now reads CLINE_API_MODEL when
  CLINE_API_KEY is present and defaults the base URL to https://api.cline.bot/api/v1.
- providerProfiles.ts: CLINE_API_KEY is mirrored into startup/profile env for
  clinepass and custom profiles at api.cline.bot, and is checked in
  isProcessEnvAlignedWithProfile.
- parse.ts: toIsoDate drops invalid resetsAt values instead of echoing them.
- Add fetchClinePassUsage tests covering auth, headers, non-OK responses, and errors.
- Add routeMetadata and providerConfig regression tests for ClinePass env/model wiring.
- Add providerProfiles tests for CLINE_API_KEY propagation in apply/env and startup persistence.
- Remove extra blank line in .env.example.

Validation:
- bun run typecheck
- bun run build
- bun run test:provider (972 pass)
- bun run check (5346 pass; 1 unrelated Windows file-mode failure in branch.test.ts)

* fixup: address PR review findings for ClinePass env routing and profile persistence

- routeMetadata.ts: Let the concrete OPENAI_BASE_URL match win before falling
  back to activeProfileProvider / activeProfileBaseUrl. Prevents a saved ClinePass
  profile from overriding an explicit CLAUDE_CODE_USE_OPENAI env pointing at
  another gateway.
- providerConfig.ts: Gate the ClinePass model branch behind !isGithubMode so a
  stale CLINE_API_KEY/CLINE_API_MODEL does not override GitHub Copilot model
  selection.
- providerProfiles.ts: Introduce isClinePassProfile() predicate that uses route
  resolution (routeId === 'clinepass' || baseUrl includes api.cline.bot) and
  share it across live application, alignment, and startup persistence paths so
  saved ClinePass profiles keep the dedicated credential consistently even with
  non-default base URLs.
- Add regression tests covering all three findings.

Validation:
- bun run typecheck
- bun run build
- bun run test:provider (973 pass)

* fixup: use hostname-based ClinePass detection in providerProfiles

Replace includes('api.cline.bot') substring matching with isClinePassBaseUrl
which validates the exact hostname via URL parsing, preventing spoofed hosts
like api.cline.bot.evil.example from triggering CLINE_API_KEY mirroring.

Validation:
- bun run typecheck
- bun run build
- bun run test:provider (973 pass)

* fixup: gate ClinePass model selection on resolved base URL

Move base URL resolution before model selection in resolveProviderRequest
so effectiveClinePassMode is only active when no explicit non-ClinePass base
URL is set via options.baseUrl, OPENAI_BASE_URL, or OPENAI_API_BASE.

Previously CLINE_API_KEY=cp-key + CLINE_API_MODEL=cline-pass/qwen3.7-max
+ OPENAI_BASE_URL=https://api.openai.com/v1 would return
requestedModel=cline-pass/qwen3.7-max with baseUrl=https://api.openai.com/v1,
sending a ClinePass model ID to a non-Cline provider.

Now the resolver returns the correct OPENAI_MODEL and OpenAI base URL in that
scenario, and only uses ClinePass model/default-base when the resolved base
URL is absent or actually api.cline.bot.

Added regression tests for:
- CLINE_API_KEY + CLINE_API_MODEL + explicit OPENAI_BASE_URL
- CLINE_API_KEY + CLINE_API_MODEL + explicit baseUrl option
- CLINE_API_KEY + CLINE_API_MODEL with no base URL (ClinePass default)

Validation:
- bun run typecheck
- bun run build
- bun run test:provider (976 pass)

* fixup: default ClinePass model for blank env

Treat whitespace-only CLINE_API_MODEL as unset so OPENAI_MODEL can still provide the ClinePass model override.

When CLINE_API_KEY selects ClinePass without any model env, fall back to the ClinePass route default instead of the generic codexplan alias.

Validation:

- timeout 600 bun test src/services/api/providerConfig.test.ts

- timeout 600 bun test src/services/api/clinepassUsage.test.ts src/integrations/routeMetadata.test.ts src/services/api/providerConfig.test.ts src/utils/providerProfiles.test.ts src/integrations/compatibility.test.ts src/components/ProviderManager.test.tsx

- timeout 600 bun test src/services/api/providerConfig.test.ts src/services/api/client.test.ts src/integrations/routeMetadata.test.ts src/integrations/runtimeMetadata.test.ts src/services/api/clinepassUsage.test.ts src/services/api/minimaxUsage.test.ts src/utils/providerProfiles.test.ts

- timeout 600 bun run integrations:check

- timeout 600 bun run typecheck

- timeout 600 bun run build

- timeout 600 bun run security:pr-scan

- git diff --check origin/main...HEAD
2026-06-30 08:29:02 +08:00
BogdanandGitHub d0843bed0b fix(compaction): make snip nudges model-aware (#1816)
Scale HISTORY_SNIP context-efficiency nudges with the active model window so large-context sessions do not get prompted after fixed low token growth.

Keep existing reset behavior and add focused regression coverage for custom intervals and model-aware attachment gating.
2026-06-30 08:28:18 +08:00
0xfandomandGitHub 185ffea893 fix(ollama): cap qwen3-coder-next:cloud output at 32768 (#1814)
* fix(ollama): cap qwen3-coder-next:cloud output at 32768

Ollama Cloud rejects qwen3-coder-next:cloud requests with
max_tokens above 32768 (400). The shared qwen3-coder-next descriptor
stays at its 65536 default; add a gateway catalog override so only the
:cloud variant is capped to 32768, with the 262144 context window
inherited from the descriptor. A local qwen3-coder-next run keeps the
descriptor default.

Re-submission of the abandoned #1133, with the deepseek-v4-pro:cloud
trailing comma kept and one added after the new entry's notes.

* test(discovery): include qwen3-coder-next:cloud in the ollama stale-cache fixture

The new gateway catalog entry adds qwen3-coder-next:cloud to the ollama
route, so the stale-cache discovery fixture must list it alongside
deepseek-v4-pro:cloud and the discovered model.
2026-06-30 08:27:35 +08:00
keyarrandGitHub 5a7969785a feat: add /set-context-window and /clear-context-window commands (#1810)
* feat: add /set-context-window and /clear-context-window commands

Add session-scoped context window overrides for OpenAI-compatible models
that fall back to the default 128k context window.

- /set-context-window [model] <tokens>: set override for current model
- /clear-context-window [model]: clear override(s)
- Overrides are in-memory, per-session, and die with the process
- Auto-compact and all context display commands respect the override
- Minimum 32k tokens to avoid auto-compact floor paradox

* fix: tighten parsing, fix minimum floor, add tests

- Reject extra arguments in /set-context-window (require exactly 1 or 2 tokens)
- Reject non-integer tokens like '64000foo' with strict regex check
- Fix MIN_CONTEXT_WINDOW_OVERRIDE from 32k to 33k to match documented floor
- Add 8 tests for session override set/get/clear/normalization/precedence

* fix: test isolation, integer validation, env override precedence test

- Add clearSessionContextWindowOverride to beforeEach/afterEach for test isolation
- Add Number.isInteger check to setSessionContextWindowOverride
- Split regression test into env override and unknown model fallback cases
- Add test for fractional values (64000.5)

* fix: provider-qualified model isolation, session cleanup, model resolution

- Store full normalized model name (lowercase only, no prefix stripping)
- Lookup tries full name first, then stripped prefix fallback
- Provider-qualified names (zai-org/glm-5.2) no longer collide with unqualified (glm-5.2)
- Clear overrides in clearSessionCaches() so /clear resets state
- Resolve active model via getMainLoopModel() instead of raw context.options.mainLoopModel
- Add tests for known model precedence, provider isolation, session isolation

* fix: use session-active model, clear both exact and stripped keys

- Revert to context.options.mainLoopModel for session-active model (not global getMainLoopModel)
- clearSessionContextWindowOverride now deletes both exact and stripped-prefix keys
- Add regression test: clearing openai/gpt-4o also clears gpt-4o fallback

* fix: store both exact and stripped keys on write for symmetric lookup

- setSessionContextWindowOverride now stores both normalized and stripped-prefix keys
- Reading openai/gpt-4o via gpt-4o now works on first query (no fallback needed)
- Update test: provider-qualified writes now create both keys (symmetric with clear)

* test: canonicalize session context overrides and fix mixed-order alias tests

* test: verify CLAUDE_CODE_MAX_CONTEXT_TOKENS precedence over session overrides
2026-06-30 08:24:50 +08:00
0xfandomandGitHub 1cdca1cc7b fix(remote-session): match ingress host by hostname, not raw substring (#1792)
* fix(remote-session): match ingress host by hostname, not raw substring

isRemoteSessionLocal and isRemoteSessionStaging decided which Claude AI
base URL receives remote-session traffic (production, staging, or
http://localhost:4000) by testing whether the ingress URL contained the
substring 'localhost' / 'staging' anywhere in the string. A production
ingress URL that merely carried those words in its path or query — e.g.
https://claude.ai/code/x?ref=staging — was misrouted to the staging or
local-dev endpoint.

Parse the ingress hostname and match it precisely instead: localhost or
127.0.0.1 for local (mirroring isLocalhostBaseUrl), and a 'staging'
hostname label for staging. A malformed URL yields no match. The
session-id (_local_ / _staging_) signals are unchanged, and the real
local/staging hosts still resolve correctly. Add product.ts tests.

* fix(remote-session): allowlist real staging ingress hosts

The dot-label match (`hostname.split('.').includes('staging')`) both
missed the real default staging ingress host `api-staging.anthropic.com`
(no bare `staging` label, so staging remote-control links fell back to
production claude.ai) and over-matched unrelated hosts such as
`foo.staging.example.com`. Replace it with an explicit allowlist: exact
`api-staging.anthropic.com` or a subdomain of the `.staging.ant.dev`
zone. Add regression coverage for both directions.
2026-06-30 08:24:10 +08:00
0xfandomandGitHub 8859c5d6e2 fix(plugins): treat prototype-shadowing marketplace names as not found (#1787)
* fix(plugins): treat prototype-shadowing marketplace names as not found

Marketplace names are user-supplied and used directly as object keys for
membership and lookup (config[name]) throughout marketplaceManager. The config
came back as a normal object, so a name shadowing an Object.prototype member —
constructor, __proto__, toString, hasOwnProperty, valueOf — resolved up the
prototype chain to a truthy inherited value. 'claude plugin marketplace remove
constructor' therefore skipped the "not found" guard and acted on a bogus
inherited entry: it crashes on entry.installLocation when a plugin seed dir is
configured, or silently no-ops the removal otherwise, instead of reporting that
no such marketplace exists.

Return a null-prototype object from loadKnownMarketplacesConfig (and its safe
variant) so every config[name] lookup across this module is own-property exact.
Add a regression test covering removeMarketplaceSource for the shadowing names.

* fix(plugins): harden cache-only marketplace lookups against prototype names

getMarketplaceCacheOnly and getPluginByIdCacheOnly parsed
known_marketplaces.json directly with jsonParse, leaving the result on
the normal Object prototype. A marketplace name shadowing a prototype
member (constructor, __proto__, ...) resolved to the inherited value, so
the not-found guard was skipped and the readers took a bogus path. Route
both through toNullProtoConfig so the lookup matches the rest of the
module. Add cache-only regression coverage and assert the removal path
never rms a directory derived from an inherited entry.

* docs(plugins): reunite loadKnownMarketplacesConfig JSDoc with its function

The toNullProtoConfig helper and its comment were inserted between the
loader's JSDoc block and the loader, orphaning the documentation. Move
toNullProtoConfig (and its explanation) above the loader's JSDoc so the
doc sits directly on loadKnownMarketplacesConfig again, and cross-link
the null-prototype rationale from the loader doc.

* test(plugins): assert getMarketplaceCacheOnly never reads a bogus cache path

The direct getMarketplaceCacheOnly proto-name test only asserted a null
result, which the unhardened path also yields (it reaches
readCachedMarketplace on an inherited entry, which then fails and returns
null). Add a deterministic regression signal: seed a sentinel
installLocation on Object.prototype so a removed null-prototype guard
would drive the reader into a cache path derived from the bogus inherited
entry, and assert no such path is ever read. Fails if the
toNullProtoConfig wrapper is dropped from getMarketplaceCacheOnly alone.
2026-06-30 08:22:53 +08:00
BogdanandGitHub 9bf6aa2308 feat(session): add branch command for conversation forks (#1808)
* feat(session): add branch command for conversation forks

* test(session): harden branch test cache cleanup

* test(session): isolate branch loader checks

* test(session): guard branch project cache cleanup
2026-06-29 18:06:28 +08:00
259c7ec27a fix(ollama): preserve chat history with native context (#1805)
* fix(ollama): preserve chat history with native context

Route Ollama chat requests through the native /api/chat endpoint so OpenClaude can send request-level options.num_ctx instead of relying on Ollama's OpenAI-compatible shim.

Default the Ollama request context to 32768 tokens, support OPENCLAUDE_OLLAMA_NUM_CTX and OLLAMA_CONTEXT_LENGTH overrides, and map max tokens/temperature/top_p into native Ollama options.

Adapt native Ollama streaming and non-streaming responses back into the existing OpenAI-shaped conversion pipeline, including usage, text, structured tool calls, and tool_use stop reasons.

Normalize native Ollama request messages for images and historical tool calls, avoiding OpenAI-only image_url/id/type payload fields in /api/chat requests.

Add Ollama context diagnostics, loopback-only ollama ps status checks, regression coverage, and documentation for verifying active context length.

* fix(ollama): address native routing review feedback

* fix(ollama): restrict loopback host matching

* fix(ollama): exclude wildcard bind address

* fix(ollama): keep https localhost proxies on chat completions

---------

Co-authored-by: jatmn <12479882+jatmn@users.noreply.github.com>
2026-06-29 18:04:42 +08:00
b15660e388 chore: remove unused atomic chat python helper (#1804)
Remove the unused top-level Python Atomic Chat helper and its dedicated pytest coverage. The active Atomic Chat provider path is implemented in TypeScript through the OpenAI-compatible shim, so keeping this standalone Python module only adds dead code.

Leave the remaining Python helper modules, pytest suite, and CI workflow intact.

Co-authored-by: jatmn <12479882+jatmn@users.noreply.github.com>
2026-06-29 17:58:49 +08:00
ff8d47d6c5 fix(status-line): estimate usage for unsupported providers (#1803)
Treat all-zero assistant usage from providers that do not report token counts as unsupported usage instead of real zero usage. Estimate current input and output tokens from the transcript, mark the values as estimated in the status-line payload, and use estimated totals whenever the active provider lacks usage data so mixed-provider sessions do not show stale cumulative totals.

Preserve tiny nonzero context percentages in the status JSON and render them as ctx <1% in the built-in status line instead of rounding back to 0%. Update the /statusline setup schema and add focused regression coverage for normal reported usage, unsupported all-zero usage, stale-provider fallback avoidance, tiny percentage display, and context percentage rounding.

Validation run locally with Bun 1.3.13: bun run build, bun run smoke, bun run typecheck, bun run typecheck:type-tests, focused tests for tokens/context/BuiltinStatusLine, bun run check, bun run test:provider, bun run test:provider-recommendation, python -m pytest -q python/tests, and bun run security:pr-scan -- --base upstream/main.

Co-authored-by: jatmn <12479882+jatmn@users.noreply.github.com>
2026-06-29 17:57:58 +08:00
BogdanandGitHub a47493342f feat(report): generate deterministic session task reports (#1802)
* feat(report): generate deterministic session task reports

* fix(report): address task report review findings

* fix(report): stabilize task report paths on Windows

* test(report): expect redacted git metadata cwd

* test(report): assert literal redacted git cwd

* fix(report): capture PowerShell and backgrounded validations

* fix(report): detect quoted validation commands

* fix(report): reconcile background validation notifications

* fix(report): keep foreground command statuses authoritative

* test(report): assert command status precedence
2026-06-29 17:57:09 +08:00
BogdanandGitHub 8023356841 feat(session): harden fork-session branching (#1801)
* feat(session): harden fork-session branching

Add explicit fork-session branching metadata, preserve fork-owned transcript state, and seed retained content replacement records for forked resumes.

Document --fork-session behavior and cover forked resume transcript/materialization behavior with focused tests.

* fix(session): respect print persistence for fork seeding
2026-06-28 06:17:29 +08:00
BogdanandGitHub 320d63c812 fix(compaction): skip microcompact when compaction is off (#1800)
* fix(compaction): respect disabled microcompact setting

Skip automatic query-loop microcompact when the message-count compaction threshold is explicitly set to off, while preserving default, numeric, and explicit compact behavior.

* test(query): isolate auto-compact config regression

* test(query): type auto-compact deps fixture
2026-06-28 06:16:42 +08:00
JATMNandGitHub ab8645da9f fix: OpenClaude native launcher after Linux install (#1798)
* fix linux openclaude native install launcher

Create the package-aware OpenClaude launcher for native installs while keeping the downloaded native payload name unchanged.

Repair the native launcher after npm cleanup with a relink-only helper so npm uninstall cannot remove ~/.local/bin/openclaude.

Update protocol registration and cleanup fallbacks to use the OpenClaude command name, with regression coverage for install surfaces.

* test linux install repair flow
2026-06-28 06:16:04 +08:00
JATMNandGitHub 13cf30afa4 fix(moonshot): Add verified Kimi effort metadata (#1796)
* Add verified Kimi effort metadata

Add kimi-k2.7-code to the Kimi Code gateway catalog and the direct Moonshot catalog, ordered newest/highest first.

Annotate Moonshot and Kimi Code Kimi models with context/output limits, reasoning capabilities, and reasoning_effort metadata limited to the verified low/medium/high levels.

Add runtime metadata, effort resolution, and provider override tests covering catalog order, qualified aliases, stale xhigh clamping, and Kimi Code request serialization.

Validation run locally: bun run build; bun run smoke; bun run check; bun run test:provider; bun run typecheck; bun run integrations:check; py -3.13 -m pytest -q -p no:cacheprovider python/tests; bun run typecheck:type-tests; bun run test:provider-recommendation; bun run security:pr-scan; focused effort/runtime/client/providerConfig tests; git diff --check.

* Fix Kimi K2.7 PR review findings

Clamp Kimi K2.7 max output metadata to 32,768 for Kimi Code, direct Moonshot, and Atlas Cloud so runtime limits do not advertise the context window as a generation cap.

Update the Moonshot runtime metadata assertion to the corrected cap and keep Hicap's separate K2.7 cap unchanged.

Restore globalThis.fetch in the Kimi Code providerOverride test with try/finally to avoid cross-test leakage.

Validation: bun test --feature=UNATTENDED_RETRY src/integrations/runtimeMetadata.test.ts src/services/api/client.test.ts src/utils/effort.codex.test.ts; bun run integrations:check; bun run build; bun run test:provider; bun run typecheck; git diff --check; bun run check.

* Fix Moonshot K2.6 and K2.5 output caps

Raise direct Moonshot Kimi K2.6 and K2.5 max output metadata from the default token count to the provider context-budget ceiling.

Add runtime metadata assertions so direct Moonshot K2.6 and K2.5 keep the 262,144 output cap while K2.7 Code remains capped at 32,768.

Validation: bun test --feature=UNATTENDED_RETRY src/integrations/runtimeMetadata.test.ts; bun run integrations:check; bun run typecheck; git diff --check; bun run build.

* Clamp Atlas Kimi K2.7 effort levels

Remove xhigh from the Atlas Cloud moonshotai/kimi-k2.7-code metadata so the gateway no longer advertises an unverified effort level for that model.

Update providerOverride and effort metadata tests to cover the exact Atlas K2.7 path and assert stale xhigh settings clamp to high.

Validation: bun test --feature=UNATTENDED_RETRY src/services/api/client.test.ts src/utils/effort.codex.test.ts src/integrations/runtimeMetadata.test.ts; git diff --check; bun run integrations:check; bun run typecheck; bun run build.

* Address Kimi metadata review findings
2026-06-27 11:25:06 +08:00
JATMNandGitHub 6f794f4185 fix(xAI): Update xAI model metadata and effort handling (#1795)
* Update xAI model metadata and effort handling

Refresh xAI direct provider catalog with live Grok 4.3, Grok Build, and Grok 4.20 model metadata, aliases, context windows, and output caps.

Align Atlas Cloud xAI entries with the refreshed model aliases and verified effort semantics.

Treat Grok Build as a coding model without reasoning effort support while keeping its xAI Responses endpoint override.

Update xAI defaults, display names, alias descriptor lookup, and regression coverage for runtime metadata, effort selection, context limits, model options, and vision capability lookup.

Validated with bun run integrations:check, focused provider/model tests, bun run typecheck, bun run build, and git diff --check.

* Address xAI PR review findings

Add the Grok 4.20 max output cap to both xAI descriptor paths and assert the runtime metadata.

Keep route-less vision catalog lookup from resolving aliases globally while preserving route-specific alias resolution.
2026-06-27 09:26:46 +08:00
BogdanandGitHub eea0a1a740 feat(cli): add headless heartbeat for print mode (#1789)
* feat(cli): add headless heartbeat for print mode

* fix(cli): harden heartbeat validation and predicates

* fix(cli): align print heartbeat phases

* fix(cli): keep heartbeat payloads schema-valid

* fix(cli): delay stream-json heartbeat until drain

* test(sdk): cover heartbeat placeholder identifiers

* fix(cli): clamp heartbeat durations

* fix(cli): ignore file persistence final events

* test(cli): cover post-turn final filtering

* fix(cli): harden headless heartbeat follow-up

Export the heartbeat SDK message type from generated core types.

Keep heartbeat cleanup paired with setup and streaming failures, and cover timing/count edge cases with focused regression tests.

* test(sdk): exercise generated heartbeat types

Expose the SDK type generator as a pure helper so tests compare fresh output with the checked-in generated artifact.

* fix(scripts): canonicalize sdk type generator entrypoint

Compare real paths for direct script execution so symlinked invocations still run the generator.

* test(sdk): harden generator import coverage

Normalize generated type freshness checks across line endings and keep the SDK type generator import-safe for non-file entrypoints.

* test(sdk): assert generator import has no write side effects

Snapshot the generated SDK type artifact around the non-file import regression so importing the generator cannot silently rewrite the committed output.
2026-06-27 09:22:25 +08:00
NikhilandGitHub 0b4000042e fix(bash): correct off-by-one in truncated-line count (#1786)
* fix(bash): correct off-by-one in truncated-line count

formatOutput reported one more truncated line than were actually
omitted. The line straddling the cut has its head shown in the kept
output, but counting newlines from maxOutputLength and adding one
counted that partially-shown line as truncated. Drop the +1 so the
"[N lines truncated]" marker counts only fully-omitted lines, and add a
regression test for the previously untested truncation branch.

* fix(bash): count the line omitted at a truncation boundary

When the cut lands exactly on a line boundary (the char before maxOutputLength
is a newline), the line before it is shown in full and the next line is fully
omitted but has no preceding newline in the omitted suffix, so the newline count
missed it. Add that line back when cutAtLineBoundary, leaving the mid-line
(straddle) case unchanged. Adds a boundary regression test alongside the
straddle one.
2026-06-27 00:28:49 +08:00
euxaristiaandGitHub 9c298112dc feat(claude): add Opus 4.8 model support (#1769)
* feat(claude): add Opus 4.8 model support

Adds Claude Opus 4.8 alongside 4.7 in the model registry, picker,
pricing, integrations, and 1M-context support. Mirrors the established
4.7 pattern so longer suffixes resolve first in canonical-name matching.

- configs: CLAUDE_OPUS_4_8_CONFIG + opus48 registry entry
- model.ts: canonical resolver, default-model dispatch (1P -> 4.8,
  3P bumped to 4.7), display & marketing names
- modelOptions: getOpus48Option in PAYG 1P/3P, opusplan description
- modelCost: COST_TIER_5_25 pricing
- context: 1M-capable assertion
- prompts: FRONTIER_MODEL_NAME -> Opus 4.8
- integrations: hicap gateway, nearai brand/vendor/model entries
- claude brand catalog: new defineModel block

Tests: extends modelSupports1M coverage for 4.8.

* fix(models): gate Opus 4.8 out of PAYG 3P picker until rollout

Opus 4.8 was being added to the third-party (3P) model picker while
getDefaultOpusModel() keeps non-first-party usage on Opus 4.7. Remove the
3P option until 3P rollout is active; first-party picker is unaffected.

Addresses CodeRabbit review on #1769.

* test(integrations): cover NearAI anthropic/claude-opus-4-8 route

Adds focused regression coverage for the new Opus 4.8 provider/model
path: asserts the NearAI vendor catalog exposes the
anthropic/claude-opus-4-8 entry and that it resolves through its
modelDescriptorId to a registered NearAI model descriptor (vendor/brand
nearai, correct default model + label), plus the NearAI route base URL.
Addresses CodeRabbit's [Minor] request to test the exact route.

* fix(models): wire Opus 4.8 into adaptive thinking, 3P fallback, and knowledge cutoff

Addresses jatmn's review findings on #1769.

- [High] thinking.ts: add opus-4-8 to the adaptive-thinking allowlist.
  Without it, claude-opus-4-8 hit the generic opus exclusion and returned
  false, dropping first-party Opus 4.8 into budget-based thinking instead of
  thinking: { type: 'adaptive' }. Adds a regression test (provider mocked to
  a non-1P value so the allowlist is the only reason 4.8 returns true).
- [Medium] validateModel.ts: add an opus-4-8 -> opus47 entry to
  get3PFallbackSuggestion so an unavailable Opus 4.8 selection suggests 4.7.
- [Medium] prompts.ts: getKnowledgeCutoff now returns "January 2026" for
  claude-opus-4-8 and claude-opus-4-7 instead of falling through to the
  stale generic "January 2025".
- [Low] modelOptions.ts: update the PAYG 1P picker comment to include Opus 4.8.

The betas.ts structured-outputs / auto-mode allowlists are intentionally left
unchanged for this PR's scope (4.7 is also absent; auto mode is gated on PI
safety probes) — to be revisited with safety-research before enabling.

* fix(models): update remaining model-launch markers for Opus 4.8 default

Addresses jatmn's follow-up review on #1769 — markers missed when Opus 4.8
became the default.

- [P2] commitAttribution.ts: add explicit `opus-4-8` and `opus-4-7` branches to
  sanitizeModelName before the broad `opus-4` fallback, so commit/PR attribution
  shows the real model instead of `claude-opus-4`. Adds a focused regression
  test (commitAttribution.modelName.test.ts; mutation-checked).
- [P2] attribution.ts: update the unknown-first-party-model co-author fallback
  from 'Claude Opus 4.6' to 'Claude Opus 4.8', and the matching test expectation.
  Also fixed a sibling de-dup test that was passing only by coincidence (it hit
  the 4.6 fallback): point it at a model the public-name map actually recognizes
  (dot form) so it exercises the real prefix-dedup path.
- [P3] fastMode.ts: FAST_MODE_MODEL_DISPLAY 'Opus 4.6' -> 'Opus 4.8'.
- [P3] context.test.ts: update the stale modelSupports1M test title/comment from
  Opus 4.7 to 4.8 (the current first-party default).

* test(models): pin the claude-opus-4-7[1m] sanitizeModelName mapping too

CodeRabbit follow-up on #1769: the test covered the suffixed 4.8 path but not the
4.7 branch with the same [1m] session suffix. Add the claude-opus-4-7[1m] case so
both newly added mappings are pinned.

* fix(models): extend fast-mode + default-effort gates to the current default Opus

Addresses jatmn's review on #1769 — two predicates still gated to opus-4-6 only
while the default Opus is now 4.8.

- [P1] fastMode.ts: isFastModeSupportedByModel returned true only for opus-4-6,
  so for Max/Team Premium users on claude-opus-4-8 fast mode wouldn't actually
  enable even though FAST_MODE_MODEL_DISPLAY/the /fast command now say "Opus 4.8
  only". Extend the predicate to the fast-mode-capable Opus models (4.8/4.7/4.6).
- [P2] effort.ts: getDefaultEffortForModel applied the Pro/Max/Team `medium`
  default only for opus-4-6, so Pro/Max/Team sessions on the new default
  claude-opus-4-8 fell through to the generic effort path. Extend the branch to
  4.8/4.7/4.6 (per the @[MODEL LAUNCH] marker).

Adds regression tests for both (mutation-checked: reverting either predicate to
opus-4-6 only fails them).

* fix(models): wire Opus 4.8 into advisor, teammate fallback, skill vars, comments

Addresses jatmn's follow-up model-launch markers on #1769.

- [High] advisor.ts: modelSupportsAdvisor / isValidAdvisorModel only whitelisted
  opus-4-6 / sonnet-4-6, so first-party sessions on the new default
  claude-opus-4-8 reported the advisor tool unsupported. Add opus-4-8 and
  opus-4-7 to both (commands/advisor.ts and claude.ts use these centralized
  predicates, so they're covered). Adds a regression test (mutation-checked).
- [Medium] swarm/teammateModel.ts: getHardcodedTeammateModelFallback hardcoded
  CLAUDE_OPUS_4_6_CONFIG -> CLAUDE_OPUS_4_8_CONFIG, so new teammates spawn on the
  current default. Adds a first-party test case (mutation-checked).
- [Medium] skills/bundled/claudeApiContent.ts: SKILL_MODEL_VARS OPUS_ID/OPUS_NAME
  4.6 -> 4.8 (the bundled claude-api skill docs don't hardcode 4.6 elsewhere).
- [Low] effort.ts + figures.ts: refresh stale "max is Opus 4.6 only" comments to
  reflect the 4.8/4.7/4.6 runtime behavior.

* test(swarm): assert provider-aware teammate fallback for Bedrock too

CodeRabbit follow-up on #1769: add a non-first-party case so the provider-aware
fallback is covered. Bedrock resolves to the Opus 4.8 Bedrock model id.

* fix(models): give Opus 4.8/4.7 the elevated output-token limits and 3P fallback chain

Addresses jatmn's review on #1769.

- context.ts: getModelMaxOutputTokens only gave opus-4-6 the 64k/128k branch, so
  opus-4-7/4-8 fell through to the generic opus-4 branch and capped at 32k —
  including the new first-party default Opus 4.8. Extend the elevated branch to
  4.8/4.7/4.6. Adds a regression test (mutation-checked).
- errors.ts: get3PModelFallbackSuggestion had chains for opus-4-6/sonnet but not
  opus-4-8/4-7, so the error path suggested no fallback for the new default while
  validateModel.ts already does. Add opus-4-8 -> opus47 and opus-4-7 -> opus46 to
  mirror validateModel.ts.

* fix(models): allow structured outputs on Opus 4.8/4.7

Addresses jatmn's finding #2 on #1769. modelSupportsStructuredOutputs whitelisted
opus-4-1/4-5/4-6 but not 4-7/4-8, so first-party/Foundry requests on the new
default Opus 4.8 lost the structured-output support that 4.6 had. Add
claude-opus-4-7 and claude-opus-4-8 to the allowlist (4.6 supports it, so the
newer Opus models do too). Adds a first-party regression test (mutation-checked).

Auto-mode (modelSupportsExternalAutoMode) is intentionally left unchanged — it is
gated on separate safety review and was not part of this finding.

* fix(models): extend file-read mitigation exemption and effort callout to Opus 4.8/4.7

Addresses jatmn's remaining findings on #1769.

- [P2] FileReadTool.ts: MITIGATION_EXEMPT_MODELS only held claude-opus-4-6, so the
  new default claude-opus-4-8 got the cyber-risk reminder appended to every file
  read that 4.6 did not — a behavioral regression. Add claude-opus-4-8 and
  claude-opus-4-7 so the recent Opus models inherit 4.6's exemption.
- [P3] EffortCallout.tsx: shouldShowEffortCallout gated the medium-effort-default
  notification to opus-4-6 only; the same default now applies to opus-4-8, so
  users on the new default never saw it. Extend the gate to 4.8/4.7/4.6. Adds a
  regression test (mutation-checked).

* fix(models): resolve Opus 4.6→4.8 drift in cost tracking, notifications, and picker strings

Addresses jatmn's review on #1769 — remaining model-launch drift now that the
first-party default is Opus 4.8.

- [P1] modelCost.ts: getModelCosts only applied the elevated fast-mode tier to
  opus-4-6, so fast-mode Opus 4.8 was billed at the normal COST_TIER_5_25 rate
  while the picker advertised the fast-mode $30/$150 price. Extend the fast-mode
  cost check to the fast-mode-capable Opus models (4.8/4.7/4.6). Non-fast usage is
  unchanged (all three already map to COST_TIER_5_25). Adds a regression test
  (mutation-checked).
- [P2] useModelMigrationNotifications.tsx: "Model updated to Opus 4.6" -> 4.8
  (the migration lands users on the opus alias = 4.8 for first party).
- [P2] commands/model/model.tsx: the 1M-unavailable error said "Opus 4.6"; made
  it generic ("Opus with 1M context...") since the gate matches any opus[1m].
- [P2] modelOptions.ts: getOpus46_1MOption is now provider-aware (3P → Opus 4.6,
  first-party → Opus 4.8); getMaxOpus46_1MOption (always first-party) → Opus 4.8.
- [P3] migrateLegacyOpusToCurrent.ts: corrected the stale comment (opus alias
  resolves to 4.8, not 4.6).

* docs(notifs): correct Opus default comment to 4.8 for 1P

Comment said 4.6 but the migration notification text and the opus alias both resolve to Opus 4.8 for first-party users. Addresses jatmn P3 review note.

* fix(integrations): remove duplicate claude-opus-4-8 descriptor

A second claude-opus-4-8 entry (vendorId anthropic) with downgraded 200k/8192 specs duplicated the canonical 1M/128k descriptor. The artifact generator rejects duplicate (id, vendorId) pairs, so integrations:generate failed and smoke-and-tests could not pass. Removed the duplicate; the canonical entry and checked-in generated artifacts are unchanged. Addresses jatmn P1.

* fix(models): address Opus 4.8 review — extra-usage label, callout test, stale copy

- isBilledAsExtraUsage: recognize opus-4-7/4-8 1M variants, not just 4.6, so the
  "Billed as extra usage" label shows for the new default and 3P default
- EffortCallout modelGate test: drop the unreliable `?ts=` cache-busting import
  and rely on mock.module live bindings, so the gate runs against the mocked
  deps on Linux CI (where the query-tagged specifier was not re-evaluated)
- refresh stale "Opus 4.6+" effort help text and callout comments to reflect the
  recent Opus models (4.8/4.7/4.6) the gate now covers

* test(effort): make Opus 4.8 callout regression deterministic via pure predicate

The behavioral test mocked auth/config/effort and relied on the already-evaluated
EffortCallout picking up those mocks, which is order-dependent and failed only in
the full Linux CI suite (both the `?ts=` dynamic-import and the static-import
variants regressed there). Extract the model check as a pure exported
`effortCalloutCoversModel` and assert it directly with no module mocking, so the
#1769 regression is covered deterministically on every platform.

* test(effort): drop config-dependent 'opus' alias from callout regression test

The bare 'opus' alias routes through getDefaultOpusModel(), whose result is
environment/config-dependent, so `effortCalloutCoversModel('opus')` was false in a
clean Linux CI environment even though the gate logic is correct — that single
assertion was the only failure in smoke-and-tests (the explicit-id assertions
passed). Assert the gate's opus-4-8/4-7/4-6 coverage with explicit canonical model
ids (incl. a [1m] variant) instead, which is deterministic on every platform.
2026-06-26 21:44:08 +08:00
NikandGitHub 4704cbc474 fix(core): join multi-block message text with a real newline (#1793)
extractTextFromContent joined text blocks with a literal "\\n" (backslash-n)
instead of a newline. Assistant messages commonly arrive as multi-block content
arrays, and the joined text feeds conversation-arc fact extraction whose regexes
deliberately treat newlines as boundaries (e.g. env-var and URL values use
[^\\s\\n"']+). With the literal separator a value at the end of one block
absorbed the next block — e.g. API_KEY=secret123 across two blocks recorded the
knowledge-graph value as "secret123\\nand..." instead of "secret123".

Use a real newline so blocks stay separated. Adds a regression test asserting
the extracted env-var value stops at the block boundary.
2026-06-26 21:41:28 +08:00
Kevin CodexGitHubOpenClaudecoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2083d1cdff fix(openai-shim): recover GLM/Qwen XML tool calls emitted as text (#1791)
* fix(openai-shim): recover GLM/Qwen XML tool calls emitted as text

GLM/Qwen-family models routed through OpenAI-compatible gateways emit
tool calls as XML text (`<tool_call><function=…>`) instead of structured
`tool_calls`. The shim had no parser, so these leaked into visible prose
and never executed — the turn ended with no tool_use block and the agent
appeared to "forget" and stop mid-task.

Add `parseXmlToolCalls` covering the three dialects seen in the wild
(function/parameter, GLM-native arg_key/arg_value, Hermes JSON), wired
into both the streaming and non-streaming paths. The streaming path
holds back text from `<tool_call>` onward (incl. an opener split across
SSE deltas), converts it to tool_use blocks at finalize, and flips the
finish reason to tool_calls — mirroring the existing Ollama fallback.
Structured tool_calls and false-positive prose are handled losslessly.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* Update src/services/api/openaiShim.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-06-26 10:48:57 +08:00
JATMNandGitHub 618e901dd8 fix(hicap): improve model catalog and effort routing (#1790)
* Improve Hicap model catalog and effort routing

Update the Hicap gateway default to claude-opus-4.8 and add verified static catalog entries for DeepSeek V4 Pro, GLM 5.2, GPT-5.4, GPT-5.5, Grok 4.3, Kimi K2.7 Code, and MiniMax M3 with context, output, reasoning, and transport metadata.

Force Hicap GPT-5.4/GPT-5.5 through the Responses transport when chat completions would reject reasoning_effort, and shape GLM 5.2 requests with the Z.AI-compatible thinking/tool-streaming behavior.

Canonicalize discovered route aliases against static catalog entries so GLM and other aliased models do not duplicate or restart with the wrong API model, while preserving the session-level /model contract without mutating provider profile model lists.

Merge static route catalog options with scoped/profile OpenAI-compatible discovery caches and ignore stale legacy OpenAI model caches for env-only Hicap routes. Add focused Atlas Cloud and OpenRouter regressions so these catalog changes do not break other gateways.

Validation run locally: bun test --timeout 10000 --feature=UNATTENDED_RETRY src/utils/providerProfiles.test.ts src/utils/model/modelOptions.gateways.test.ts src/utils/model/modelOptions.hicap.test.ts src/utils/model/modelOptions.xiaomi-mimo.test.ts src/utils/model/modelOptions.github.test.ts; bun test --timeout 15000 --feature=UNATTENDED_RETRY src/commands/model/model.test.tsx src/commands/provider/provider.test.tsx src/services/api/client.test.ts; bun test --timeout 10000 --feature=UNATTENDED_RETRY src/services/api/providerConfig.test.ts src/services/api/providerConfig.local.test.ts src/integrations/runtimeMetadata.test.ts src/integrations/compatibility.test.ts src/integrations/routeMetadata.test.ts src/utils/providerFlag.test.ts src/utils/providerValidation.test.ts src/utils/effort.codex.test.ts; bun run typecheck; git diff --check.

* Address Hicap PR review findings

Rename the discovered model helper to reflect production use, seed ModelPicker focus from the canonical option value, normalize focused/selected picker aliases through the option list, and simplify requested API format support logic.

Validation: bun test src/components/ModelPicker.test.tsx src/services/api/bootstrap.test.ts src/services/api/providerConfig.test.ts src/services/api/providerConfig.local.test.ts; bun run typecheck

* Fix Hicap catalog id responses routing

Resolve route catalog aliases before OpenAI shim runtime/support checks so Hicap catalog ids such as hicap-gpt-5.5 are probed as gpt-5.5 and keep their required Responses API transport.

Validation: bun test src/services/api/providerConfig.local.test.ts src/services/api/providerConfig.test.ts src/services/api/openaiShim.test.ts; bun run typecheck

* Add bootstrap regression for Hicap errored discovery

Exercise fetchLocalOpenAIModelOptions with an errored Hicap discovery result so canonical catalog option mapping and duplicate removal are covered at the bootstrap payload level.

Validation: bun test src/services/api/bootstrap.test.ts; bun run typecheck
2026-06-26 10:45:52 +08:00
LM40R4YandGitHub a5a8ccc378 fix: auto-continuation overly biased toward Claude-style output — breaks with non-Claude models (#1713)
* fix: auto-continuation overly biased toward Claude-style output

When using non-Claude models (OpenAI, Gemini, local models, etc.), the
continuation detection frequently fails, causing the agent to stop after
each turn and wait for manual user input.

Changes:
- Expand CONTINUATION_SIGNALS verb list to include common verbs used by
  non-Claude models: process, download, upload, compile, train, evaluate,
  test, continue, generate, extract, merge, deploy, install, configure,
  refactor, optimize (plus their -ing forms)
- Fix COMPLETION_MARKERS check to only block continuation when no
  continuation signal is present nearby (prevents false positives when
  words like 'complete' or 'done' appear mid-sentence)
- Add soft fallback nudging when text has no terminal punctuation and
  no explicit completion signal (assumes model intends to continue by
  default)
- Increase MAX_CONTINUATION_NUDGES from 3 to 20 for longer multi-step
  tasks
- Pass 'interrupt' reason to abortController.abort() in the TUI control
  interrupt handler for consistent signal propagation

Fixes #1707

* fix: add 429 retry, disable microcompact, disable command-queue abort

- Add 429 rate limit retry with backoff for non-Claude providers
  (src/services/api/withRetry.ts)
- Disable microcompact to prevent aggressive conversation trimming
  (src/query.ts)
- Disable command-queue abort to prevent interrupting running tools
  (src/cli/print.ts)

* fix: address PR review feedback

- 429 retry: use separate counter instead of manipulating attempt
- Soft fallback: remove aggressive fallback nudging all unpunctuated text
- Microcompact: honor maxMessagesCompactionThreshold 'off' setting
- Fix typecheck issues (pendingCacheEdits, imports)

* fix: address second round of PR review

- Remove 429 retry policy (out of scope for continuation fix)
- Add presentProgressive check for 'complete. Now processing...' case
- Restore command queue preemption (split to separate PR)
- Fix PendingCacheEdits type for typecheck

* fix: address third round of PR review

- Fix completion guard condition (inverted logic — was blocking with continuation signals present)
- Narrow presentProgressive to only 'Now [verb]ing' (avoid gerund false positives)
- Restore original microcompact call (remove config gate)
- Add 'interrupt' reason to permission deny abort calls

* fix: address fourth round of PR review

- [P1] Add 'continuing with' and 'proceeding to' to strongIntent regex
  so punctuated transition phrases survive terminal-punctuation gate
- [P2] Pass 'interrupt' reason to abortController.abort() in
  PermissionContext.runHooks() for interactive permission hooks

* fix: address fifth round of PR review

- [P1] Add missing -ing gerund forms to line 15 (creating, writing,
  editing, running, checking, building, etc.) so the late gerund
  transition fix covers the original action verbs too
- [P2] Add imperative/declarative signals for bare patterns:
  'need to <verb>' ('Need to process files'),
  'now <verb>' ('Now process files', with negative lookahead
  to avoid 'Now you' false positives),
  'next (i|we) <verb>' ('Next I process files')

* fix: address sixth round of PR review

Addresses all 5 findings from jatmn's CHANGES_REQUESTED review:

1. (code-quality) Extract verb list to shared ACTION_VERBS array;
   build all continuation regexes from it via buildContinuationSignals()
2. (minor) Restrict presentProgressive to gerund forms of the
   same verb list instead of broad \w+ing
3. (nit) Remove accidental 'nul' entry from .gitignore
4. (medium) Add focused tests for new verbs, imperative patterns,
   present-progressive fallback, completion-marker guard, and
   verb-list deduplication
5. (minor) Add tests verifying MAX_CONTINUATION_NUDGES = 20
   and the guard comparison

* fix: address seventh round of PR review

Addresses all 4 findings from jatmn's CHANGES_REQUESTED review:

1. [P1] Remove dead 'take' branch from VERB_ING gerund map
2. [P2] Pass 'interrupt' reason to abort() in print.ts SIGINT handler
   and PermissionContext.ts cancelAndAbort path
3. [P3] Fix tab indentation in query.ts,
   PermissionPromptToolResultSchema.ts, and permissions.ts
4. [P3] Update PR description to match actual code changes

* fix: address PR review feedback (indentation, filter-based verb exclusion)

- Fix indentation (2-space style) in PermissionPromptToolResultSchema.ts
  else-if block (jatmn review finding 1)
- Use ACTION_VERBS.filter() instead of fragile v.replace(/^do\|/, '')
  for 'time to' regex (finding 2)
- Clarify default fallback behavior in PR description (finding 4)
- Note: queryLifecycle cleanup (finding 3) not applicable — field
  does not exist in this codebase

* fix: punctuated imperative/declarative patterns now signal continuation intent

The new imperative patterns (need to <verb>, now <verb>, next i/we <verb>)
matched in the late-window signal check but were silently dropped when
the text had terminal punctuation, because the punctuated branch only
checked strongIntent / presentProgressive / endsWithColon.

This left examples like 'Need to process files.' and 'Now create the
component.' returning shouldNudge: false, even though the bare variants
correctly returned true.

Fix: add hasImperativeSignal to the punctuated gate, re-testing lowerText
against the imperative patterns so punctuated action-intent signals are
recognized.

Also narrows the #1707 closure claim in the PR description: the default
fallback remains shouldNudge: false (the broader inversion suggested in
the issue introduced false positives in earlier rounds).

Tests added for punctuated imperative variants.

* fix: address ninth round of PR review

- [Medium] Restore abortController.abort('interrupt') in onInterrupt()
  bridge/SDK callback to keep interrupt-reason fix consistent
- [Minor] Normalize indent (tabs -> 2-space) in imperative/declarative
  test block in bugfixes.test.ts
- [Nit] Use lateText (last 120 chars) for hasImperativeSignal check
  for consistency with surrounding late-window logic
- queryLifecycle removal is intentional cleanup from earlier round;
  no longer referenced in callModel options

* fix: tighten need-to pattern to exclude subject-led advice (You need to...)

CodeRabbit review flagged that the `need to` continuation pattern was
overmatching subject-led advice like "You need to update..." causing
false-positive nudges. Added negative lookbehind `(?<!\b(?:you|i|we|...)\s+)`
to both `CONTINUATION_SIGNALS` and `hasImperativeSignal` so only bare
imperatives (no subject) trigger continuation. Added regression tests.

Also confirmed three earlier findings were already fixed:
- print.ts onInterrupt() already uses abort('interrupt') 
- Indentation in bugfixes.test.ts:172-200 already 2-space 
- hasImperativeSignal already uses lateText 
- queryLifecycle removal was intentional cleanup 
2026-06-26 10:14:34 +08:00
22fa5b4227 fix(copilot): auto-refresh Copilot token on 401 instead of only showing re-auth hint (#1766)
* fix(copilot): auto-refresh Copilot token on 401 instead of only showing re-auth hint

When GitHub Copilot returns 401 'token expired', the existing flow only
showed a hint to run /onboard-github (the #1042 fix) but never actually
refreshed the token mid-session.

Now _doRequest detects the 401 + 'token expired' body in GitHub mode,
calls refreshCopilotTokenOn401() to exchange the stored OAuth token for
a fresh Copilot token, updates process.env, and retries the request.

Fixes #1746

* fix: promote refreshedCopilotToken in auth precedence and add 401 retry integration test

- Fix auth value precedence in buildHeadersForAttempt so refreshedCopilotToken
  takes effect even when a CredentialPool is active (single key still creates
  a pool, shadowing the refreshed token on retry)
- Add integration test verifying GitHub Copilot 401 'token expired' triggers
  refreshCopilotTokenOn401() and retries chat_completions with the new token
  (P2 for PR #1766 review)

* test: add codex_responses 401 retry integration test (CodeRabbit P2)

Adds a new test 'GitHub Copilot 401 codex_responses retries with refreshed
token' that verifies the 401 retry path for the codex_responses transport
(lines 2349-2390). Mocks performCodexRequest to throw APIError(401, 'token
expired') on first call and return a valid SSE response on second, verifies
refreshCopilotTokenOn401 is invoked and the retry uses the refreshed token.

* review: shared token-expired helper, credential-pool test, edge case tests

Finding 1: Promote token-expired detection into isCopilotTokenExpiredError()
helper used by both chat_completions and codex_responses paths.

Finding 2: Add credential-pool integration test verifying that
refreshedCopilotToken takes precedence over credentialPool.next() when
OPENAI_API_KEYS is active.

Finding 3: Add edge case tests - 'token has expired' variant, 401 without
expired substring (no refresh), and same-token refresh (retries with original
token, exhausted, then fails).

* review: credential source check in codex retry path, fix mock leak

- Add credential source gate (apiKey === process.env.OPENAI_API_KEY) to
  codex_responses 401 retry path, matching the chat_completions path guard
- Add same-token guard (newApiKey !== apiKey) after refresh in codex path
- Remove leaking mock.module('./codexShim.js') from providerOverride test,
  use fetch mock instead so performCodexRequest converts 401 to APIError
- Remove duplicate old test that was left behind by previous edit
- Add providerOverride test verifying the credential source gate blocks refresh

* review: P2 refreshedCopilotCodexToken, P3 guard continue on last attempt

P2: Add refreshedCopilotCodexToken variable that takes precedence over
providerOverride.apiKey on retry, mirroring the chat path's refreshedCopilotToken
in buildHeadersForAttempt. Prevents stale override key from being reused after
a successful refresh.

P3: Guard the chat path's continue with attempt < maxAttempts - 1 so the
original 401 is surfaced when the 401 lands on the final retry slot, instead
of falling through to the generic 500 exit error.

* review: P1 chat path credential gate, P3 log refresh failures

P1: Add oldToken === (process.env.OPENAI_API_KEY ?? '') check in chat
path before calling refreshCopilotTokenOn401(), matching the codex path
guard. Prevents credential substitution when providerOverride.apiKey,
route credential, or custom auth header was the failing credential.
Add regression test for chat path with mismatched providerOverride.

P3: Log caught error with logForDebugging before returning false in
refreshCopilotTokenOn401() catch block, so secure storage read failures,
token exchange rejections, and persistence failures are visible in debug
logs instead of silently swallowed.

* fix: restrict codex refresh path to Copilot/GHE endpoints only

isGithubWithCodexTransport was using isGithubMode which is true for all
GitHub modes including GitHub Models API and custom routes. Changed to
isGithubCopilotEndpoint that checks githubEndpointType === 'copilot' ||
githubEndpointType === 'ghe', matching the chat path's isGithubCopilot
guard. Prevents COPILOT_HEADERS and refreshCopilotTokenOn401 from being
triggered for non-Copilot endpoints.

* fix: move didRefreshCopilotToken assignment after credential source check

The refresh flag was set before confirming oldToken matches
process.env.OPENAI_API_KEY, burning the single refresh chance when the
credential pool rotates to a non-matching key first. Moved inside the
credential source check so it's only consumed for eligible credentials.

* fix: add oldToken guard to chat-path credential source check

Prevents '' === '' match when both Authorization and OPENAI_API_KEY are
empty from burning the refresh flag on an unauthenticated request.

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
2026-06-26 06:53:19 +08:00
JATMNandGitHub 0071454479 Update Xiaomi MiMo v2.5 catalog reasoning (#1788)
Limit the direct Xiaomi MiMo and token-plan catalogs to the supported v2.5 chat models, removing the deprecated v2/flash entries.

Add OpenAI-compatible reasoning_effort metadata for low/medium/high on Xiaomi MiMo v2.5 models while keeping the provider on the existing OpenAI-compatible path.

Add tests covering the token-plan catalog, raw api-key auth header, and reasoning_effort request serialization.

Validation: bun run build; bun run smoke; bun run check; bun run test:full; bun run test:provider; bun run test:provider-recommendation; bun run typecheck; bun run typecheck:type-tests; python -m pytest -q python/tests; bun run integrations:check; focused Xiaomi/OpenAI shim tests; git diff --check; bun run scripts/pr-intent-scan.ts --base upstream/main.
2026-06-25 23:32:39 +08:00
JATMNandGitHub 40faf256db fix(atlas-cloud): vendor to gateway catalog correction and added reasoning support (#1785)
* Fix Atlas Cloud gateway catalog

Move Atlas Cloud from a direct vendor descriptor to a hosted OpenAI-compatible gateway while preserving the atlas-cloud preset, dedicated ATLAS_CLOUD_API_KEY handling, model env defaults, and generated integration metadata.

Add shared upstream model descriptors needed by the Atlas gateway catalog so Atlas entries do not own model metadata or resolve through NearAI-scoped descriptors.

Add conservative Atlas reasoning metadata for verified Kimi effort levels and Grok Build always-on/no-wire reasoning, plus resolver and OpenAI shim tests for top-level reasoning_effort serialization and no-wire behavior.

* Add verified Atlas reasoning effort metadata

Update the Atlas Cloud gateway catalog with route-specific reasoning controls verified through live Atlas probes.

Enable top-level reasoning_effort metadata for models that accepted low/medium/high/xhigh, limit Doubao routes to low/medium/high, keep Grok Build as always-on no-wire reasoning, and leave OWL without effort controls after probes showed no reasoning output.

Expand effort resolver coverage and OpenAI shim serialization tests so Atlas metadata exposes the expected effort levels and strips reasoning_effort for no-wire models.

Validation: bun install; bun run build; bun run smoke; bun run check; bun run test:provider; bun run test:provider-recommendation; python -m pytest -q python/tests; bun run typecheck; bun run typecheck:type-tests; bun run doctor:runtime; bun run security:pr-scan -- --base HEAD --head 1878a1f05f7fcce7ab7098c890ff3091dafcaf66.

* Add explicit Atlas catalog aliases

Introduce opt-in catalog aliases for model entries and resolve those aliases through the active route catalog before sending OpenAI-compatible requests.

Add Atlas aliases where descriptor IDs differ from Atlas API model IDs, including GLM, Claude coding variants, GPT, Kimi, MiniMax, Qwen, and Grok entries.

Teach effort metadata lookup to use the same explicit aliases so /effort support matches routed model IDs.

Cover Atlas alias routing plus OpenRouter and Hicap no-op routing in providerConfig tests, and add Atlas GLM alias effort coverage.

* Strengthen Atlas routing regression tests

Assert the Atlas gateway catalog resolves at least one shared model before checking for NearAI descriptor leakage.

Add an Atlas full API-name fallback assertion so catalog alias routing cannot over-match provider-qualified model IDs.
2026-06-25 23:25:41 +08:00
0xfandomandGitHub 82fd23798e fix(env-file): collapse escaped backslashes in quoted values (#1773)
* fix(env-file): collapse escaped backslashes in quoted values

unescapeQuotedValue only collapsed an escaped quote (\" -> "), but the
closing-quote scanner (findClosingQuote/isEscapedQuote) already treats a
quoted value as backslash-escaped: an odd-length backslash run escapes the
following quote, so a \\ pair is consumed as a single escaped backslash
when locating the closing quote. Because the unescaper left \\ untouched,
the two stages disagreed and a value such as "a\\b" round-tripped to the
doubled "a\\b" instead of "a\b".

Collapse \\ to \ in unescapeQuotedValue so both stages share one escaping
rule. Lone backslashes before ordinary characters are still preserved.

* test(env-file): cover escaped backslash adjacent to closing quote

Locks down the terminator interaction between findClosingQuote/isEscapedQuote
and unescapeQuotedValue: a value written as "a\\" (two backslashes before
the closing quote) parses to a single trailing backslash.
2026-06-25 23:23:43 +08:00
13f7401541 perf(repl): batch streaming text, cache normalize, coalesce config writes (#1744)
Three independent runtime hot-path wins for long sessions and fast streams:

- Streaming text (REPL.tsx): the Ink root is a LegacyRoot, so every
  per-delta setStreamingText committed a synchronous REPL render. The
  preview hides the in-progress trailing line, so deltas between newlines
  changed nothing on screen yet still re-rendered. Hold the full text in a
  ref and publish to state only when the newline-truncated preview changes
  (or on clear), dropping those no-op renders under 100-300 delta/sec
  streams. Displayed text is byte-identical; the Esc-interrupt path reads
  the ref so no trailing partial line is lost.

- Incremental normalize (messages.ts + Messages.tsx): normalizeMessages
  flat-maps the whole transcript on every append (O(n) over 2,800+
  messages). normalizeMessagesCached memoizes per-message output keyed on
  (message identity, isNewChain entry flag) in a WeakMap, so unchanged
  messages are reused with stable object identity (reviving downstream
  memo/WeakMap bailouts and cutting GC pressure). It is an allocation/
  identity optimization — the call still scans the list, it is not an O(1)
  append. Proven equivalent to normalizeMessages in tests.

- Coalesced config writes (config.ts): saveGlobalConfig does a sync
  lock+reread+backup+fsync per call. saveGlobalConfigDeferred queues the
  updater, write-throughs the cache for read coherence, and folds the
  batch into one locked write on a 500ms debounce (flushed on cleanup and
  process exit). Auth-loss guard, lock and backup stay on the disk path.

Also fixes handleMessageFromStream's input_json_delta to update tool input
in place instead of reordering the updated tool to the array tail.

Review fixes (CodeRabbit + jatmn):
- REPL onStreamingText updates streamingTextRef BEFORE the showStreamingText
  guard so reduced-motion / cursor-up-yank-bug terminals preserve partial
  assistant output on Esc. The publish decision is extracted to a pure
  helper (streamingTextPublish.ts) with streamingTextPublish.test.ts, and
  replStreamingTextClear.test.ts source-scans REPL.tsx to assert both
  turn-boundary paths fully clear the streaming refs (no stale re-append).
- saveGlobalConfigDeferred primes the cache (getGlobalConfig) before
  enqueueing, and a direct saveGlobalConfig flushes pending deferred writes
  first. Together these keep same-process reads coherent: the first deferred
  counter update is visible immediately even on a cold cache, and a direct
  save can no longer clobber a queued delta with a disk snapshot. Regressions
  in deferredConfigWrites.test.ts.
- Extracted the deferred-write queue/debounce/write-through/drain into a
  generic, disk-free engine (deferredConfigWrites.ts) with injected
  storage/scheduler; deferredConfigWrites.test.ts exercises the real branch.
- config.deferredWrite.test.ts loads config via a query-suffixed specifier
  so a leaked mock.module('./config.js') can no longer silently turn its
  assertions into no-ops; it always exercises the real path.
- messages.streamingToolUses.test.ts covers interleaved input_json_delta
  order preservation; messages.normalizeCached.test.ts now covers the
  reused-message entryFlag true<->false cache transition.
- Corrected the normalizeMessagesCached comment (O(n) scan / allocation
  optimization, not an O(1) append).

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-25 14:06:46 +08:00
a723540163 perf(build): minify the CLI bundle (whitespace + syntax, keep identifiers) (#1743)
dist/cli.mjs shipped unminified at 21.7MB; whitespace+syntax minification
cuts it to ~16MB (-26%) and shaves V8 parse time on every invocation.
Identifier mangling stays off because the codebase matches
constructor.name (errors.ts, toolExecution.ts, useCanUseTool). The SDK
bundle stays unminified — its React/Ink leak check greps import syntax
that minification would rewrite.

The bundle guard's missing-module tripwire relied on Bun's
`// missing-module-stub:<path>` module-boundary comments, which
minification strips. The stub loader now also emits the marker as a
side-effecting string push (survives treeshaking and syntax-minify), and
the guard parses both forms.

Review fix (CodeRabbit + jatmn): the marker parser previously truncated
paths at the first backslash or space, so a JSON-escaped Windows marker
like "missing-module-stub:C:\\Users\\Jane Doe\\...\\src\\...\\foo.js" was
captured as a useless `C:` (or `C:\\Users\\Jane`) fragment and canonicalized
to the wrong key — letting a newly stubbed module slip past the tripwire on
Windows/spaced build hosts. Parse each marker form to its correct
terminator instead: the string literal runs to its matching (back-ref)
closing quote consuming escaped pairs, and Bun's comment runs to end of
line. Extract canonicalStub() + the parser into scripts/stubMarkerGuard.ts
so the logic is unit-testable, and add regression tests for Windows,
spaced, comment-form, and multi-marker-per-line cases.

Verified: build green, bundle ~16MB minified, guard passes against the real
bundle, stub-guard tests pass, --version works through the minified bundle.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-25 12:34:59 +08:00
a1a3cfc31e perf(integrations): load descriptor catalog lazily on first registry read (#1742)
Split the generated output into integrationManifest.generated.ts (plain
preset metadata, type-only deps) and integrationArtifacts.generated.ts
(the ~10k-line descriptor graph). Add a setRegistryLazyLoader() hook in
registry.ts so read getters evaluate the catalog on first access instead
of eagerly at module load — cutting startup cost on paths that never read
the registry (index.ts previously eval'd 60+ descriptor modules at import).

Review fixes:
- registry.ts ensureLoaded() keeps the lazy loader armed until it
  succeeds (re-entrancy guard + clear-on-success), so a throwing lazy
  import retries on the next read instead of leaving the registry empty.
- providerSecrets.ts no longer statically imports the heavy descriptor
  artifacts. It sat on the bootstrap startup chain
  (bootstrap -> providerConfig -> providerProfile -> providerSecrets), so
  the static import undercut the lazy-loading goal. Import
  PROVIDER_PRESET_MANIFEST from the lightweight manifest module and lazily
  require() the descriptor arrays inside readDescriptorCredentialEnvKeys()
  (cached by getKnownProviderSecretEnvKeys(), so it runs at most once).
- artifactGenerator.test.ts resolves artifacts by full
  src/integrations/generated/<name> path instead of tuple order; the last
  order-dependent test now uses splitGeneratedArtifacts().
- Added registry + providerSecrets regression tests (loader retry on
  failure; no static descriptor import on the bootstrap path).

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-25 11:22:13 +08:00
BogdanandGitHub 6fdb1d0c46 fix(bg): preserve provider env-file values during prompt detection (#1767) 2026-06-25 11:13:35 +08:00
BogdanandGitHub 3157ee715b Fix slash command suggestion filtering (#1664) 2026-06-25 11:13:01 +08:00
JATMNandGitHub cb689cc33a feat(effort): Add model-level reasoning effort routing (#1780)
* Add model-level reasoning effort metadata

Introduce per-model reasoning control metadata on catalog entries and model descriptors so /effort support can be expanded without provider-wide inference.

Resolve /effort through explicit model metadata first, preserve legacy allowlist behavior, and treat supportsReasoning-only entries as capability metadata that does not mutate requests.

Guard OpenAI shim effort serialization with the model-level wire support check and add focused tests for capability-only, explicit metadata, opt-out, and toggle-mode cases.

Document the reasoning metadata contract and provider follow-up workflow.

* Expand reasoning effort routing

Centralize OpenAI shim reasoning request planning so DeepSeek-compatible and Z.AI-compatible controls flow through the effort resolver instead of provider-specific shim helpers.

Add compatibility metadata handling for DeepSeek and Z.AI routes while keeping supportsReasoning-only catalog entries non-controllable until exact wire formats are verified.

Respect route removeBodyFields after compatibility serialization and make provider override support checks use the resolved override route/base URL instead of ambient provider metadata.

Document temporary compatibility rules and add regression coverage for Atlas DeepSeek, Z.AI levels, Groq stripping, providerOverride OpenAI effort, providerOverride Groq stripping, and non-generic metadata opt-out.

Verified: bun test --feature=UNATTENDED_RETRY src/utils/effort.codex.test.ts; bun test --feature=UNATTENDED_RETRY src/services/api/client.test.ts; bun test --feature=UNATTENDED_RETRY src/services/api/openaiShim.test.ts; node .\\node_modules\\typescript\\bin\\tsc --noEmit; bun run build; git diff --check.

* Fix Responses API reasoning effort shape

Send reasoning effort on OpenAI-compatible Responses requests using the nested reasoning object expected by the endpoint instead of flat reasoning_effort/reasoning_summary fields.

Keep chat_completions behavior unchanged so OpenAI-compatible chat endpoints still receive top-level reasoning_effort.

Add a regression test covering the Responses request body shape and verifying the flat fields are omitted.

Validation: bun test --feature=UNATTENDED_RETRY src/services/api/openaiShim.test.ts; node .\\node_modules\\typescript\\bin\\tsc --noEmit; git diff --check.

* Document reasoning effort metadata rules

Make the new reasoning-effort guide discoverable from the integrations overview and reading order.

Update model, gateway, and vendor onboarding docs to explain that supportsReasoning is descriptive only and does not enable /effort request mutation without verified per-model reasoning metadata.

Clarify that gateway and vendor catalogs must annotate reasoning controls per exact route/model rather than provider-wide.

Validation: git diff --check.

* Stabilize effort resolver tests

Add an optional reasoning control context so effort tests can inject provider, catalog, model descriptor, and shim metadata without mocking process-global integration/provider modules.

Update effort.codex tests to use the injected context and restore only the remaining local mocks, preventing mock leakage into later full-suite provider tests.

Validation: bun run check; bun run test:provider; bun run test:provider-recommendation; bun run typecheck:type-tests; bun run integrations:check; python -m pytest -q python/tests; bun run security:pr-scan -- --base upstream/main --head HEAD.

* Address effort PR review findings

Load integration registry before catalog reasoning lookup, isolate provider override route resolution from ambient routes, and carry explicit compat reasoning metadata into the OpenAI shim request planner.

Clarify reasoning metadata documentation and add focused regression coverage for compat metadata and provider override route preference.

* Fix Z.AI metadata high effort serialization

Include high in the Z.AI-compatible metadata gate so high-only reasoning metadata emits reasoning_effort instead of silently dropping the user-selected effort.

Add regression coverage for a high-only zai_compatible catalog entry flowing through the OpenAI shim request planner.

* Fix provider override effort fallback

Allow unrecognized providerOverride OpenAI-compatible routes to fall back to legacy effort support instead of dropping user-selected effort.

Constrain compat metadata levels to wire-faithful high/xhigh values and clarify reserved reasoning wire formats in docs and descriptors.

* Clamp provider override effort by route metadata

Resolve providerOverride effort against the override model and route context before converting it for the OpenAI shim, so stale persisted effort values respect per-model metadata levels.

Add regression coverage for high-only providerOverride metadata and explicit max filtering in compat metadata levels.
2026-06-25 10:10:53 +08:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
263f07e2ab chore(main): release 0.20.1 (#1774)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
v0.20.1
2026-06-25 09:31:29 +08:00
BogdanandGitHub db66f41071 fix(resume): tolerate malformed legacy attachment records (#1768)
* fix(resume): tolerate malformed legacy attachment records

* test(resume): allow resume hook messages in recovery test

* fix(resume): use validated attachment for skill listing
2026-06-25 09:27:02 +08:00
b8c34645c9 chore(deps): clean npm install — fix CVEs, silence warnings (#1782)
* chore(deps): clean npm install — fix CVEs, silence warnings

- bump undici 7.24.6 → 7.28.0 (7 high CVEs: TLS bypass, header injection,
  DoS, cache poisoning, SameSite downgrade, cross-origin routing)
- bump ws 8.20.0 → 8.21.0 (2 high CVEs: uninitialized memory disclosure,
  memory exhaustion DoS)
- add allowScripts for sharp + protobufjs to silence install-script warnings
- vendor node-domexception shim (re-exports native DOMException) and override
  the deprecated polyfill pulled transitively by google-auth-library →
  gaxios → node-fetch@3 → fetch-blob

Result: `npm install` reports 0 vulnerabilities, 0 warnings.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* chore(deps): update bun.lock for undici/ws bumps and node-domexception override

CI runs `bun install --frozen-lockfile`, which requires bun.lock to match
package.json. The previous commit bumped undici/ws and added the
node-domexception shim override but didn't include the regenerated lockfile,
causing frozen-lockfile CI to fail.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(publish): include vendor/node-domexception-shim in npm tarball

The file: override in package.json points at vendor/node-domexception-shim,
but the files array didn't list vendor/, so npm pack excluded it. End-user
npm installs would fail resolving the override.

Add vendor/node-domexception-shim/ to the files array. Verified via
npm pack --dry-run: tarball now contains both shim files (12 → 14 files).

Addresses reviewer finding #1.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-25 09:26:11 +08:00
BogdanandGitHub bd00b3b3c5 fix(bg): stream session logs with bounded memory (#1762)
* fix(bg): stream session logs with bounded memory

* fix(bg): handle log follow cleanup edge cases

* fix(bg): surface non-follow log read errors

* test(bg): isolate log streaming temp dirs
2026-06-25 09:10:01 +08:00
d32b6f0476 fix(update): stop false "development build" block on npm installs with NODE_ENV=development (#1781)
`/update` and `openclaude update` reported "Auto-update is unavailable for
a development build." even when OpenClaude was correctly installed from npm,
as long as the launching shell had `NODE_ENV=development` exported.

Root cause: `getCurrentInstallationType()` checked `NODE_ENV === 'development'`
as its first branch, before any path-based detection. A user's shell env var
then downgraded a real npm install to 'development', which routed
`resolveUpdateStrategy()` to `{ action: 'blocked', reason: 'development' }`.

Two-part fix:

1. doctorDiagnostic.ts — move the `NODE_ENV === 'development'` check to a
   fallback position after all real-install path markers (bundled mode, local
   npm, npm-global paths, /npm/, /nvm/, `npm config get prefix`). Path
   detection runs first; NODE_ENV only classifies as 'development' when no
   install path matches (i.e. an actual source-tree `bun run dev` run).

2. bin/openclaude — the heap-sizing relaunch previously used
   `fileURLToPath(import.meta.url)`, which resolves symlinks. After relaunch,
   `process.argv[1]` pointed at the real file target (repo path for
   `npm install -g .`, package path inside node_modules for real installs),
   defeating path-based detection. Preserve `process.argv[1]` (the original
   invocation path, e.g. /usr/local/bin/openclaude or nvm bin symlink) so
   npm-global path markers can match correctly.

Verified: `bun run typecheck` passes; `openclaude doctor` now reports
npm-global (not development) with NODE_ENV=development set on a real
npm global install.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-25 06:52:58 +08:00
BogdanandGitHub 28bbec4948 fix(memory): bound memory-directory scanning work (#1757)
* fix(memory): bound memory-directory scanning work

* fix(memory): harden bounded memory scanning follow-up
2026-06-25 06:39:05 +08:00
euxaristiaandGitHub 701b68c215 fix(query): prevent spurious Windows interruption prompt by passing 'interrupt' reason (#1733)
* fix(query): prevent spurious Windows interruption prompt by passing 'interrupt' reason

* fix(cli): pass 'interrupt' abort reason and add stop-hook regression tests

* test(query): exercise handleStopHooks abort-reason branch directly

Adds focused coverage that imports and runs handleStopHooks() while a
Stop-hook generator is being consumed, asserting abort('interrupt')
suppresses the synthetic '[Request interrupted by user]' message and a
default abort still yields it. Closes jatmn's P2 regression request.
2026-06-25 06:28:57 +08:00
euxaristiaandGitHub 669ecdfa8b fix: prevent recursive debounce infinite loop in team memory sync (#1726)
* fix: prevent recursive debounce infinite loop in team memory sync

* fix: chain cap-reschedule push after in-flight promise instead of running concurrently

* fix: preserve currentPushPromise when replaced during yield point + tests

* fix: clear pending debounce timer in _resetWatcherStateForTesting

* fix(teamMemorySync): prevent concurrent pushes, make clearing identity-safe, and avoid duplicate follow-ups

* fix(teamMemorySync): guard capped follow-up push against suppression; fix test gaps

Addresses jatmn's three P3 findings on #1726.

- watcher.ts: the capped reschedule path queues a serialized follow-up
  executePush() without consulting pushSuppressedReason, whereas schedulePush()
  short-circuits on suppression. If a permanent failure set suppression while
  the in-flight push was running, the queued follow-up fired one redundant,
  identically-failing call. Skip executePush() when pushSuppressedReason is set,
  mirroring schedulePush(). Adds a regression test (mutation-checked: removing
  the guard makes it fail).

- watcher.test.ts: the "resets to 0 when executePush completes" test was
  vacuous — rescheduleCount started at 0 and executePush() never owns that
  reset (onDebounceFire and _resetWatcherStateForTesting do). Renamed to
  "clears pushInProgress when executePush completes" and dropped the misleading
  rescheduleCount assertion; the reset path stays covered by the onDebounceFire
  cap test.

- watcher.test.ts: the top-level mock.module('./index.js') is process-global and
  mock.restore() does not undo it. Follow the spawnCtxAgent.test.ts pattern —
  (re)register the mock in beforeEach and restore the real module in afterEach —
  so it can't silently bleed into a future test that imports teamMemorySync/index.
2026-06-25 06:27:49 +08:00
euxaristiaandGitHub 02d43b6942 fix: surface swallowed error in plan file write (#1725)
* fix: surface swallowed error in plan file write

* test: add regression test for plan file write failure path

* test(ExitPlanModeV2Tool): scope fs mock to single test and pass full call signature

Two CodeRabbit concerns:

1. The global fs/promises mock in beforeAll was leaking into unrelated
   test suites (e.g. loadAgentsDir.test.ts) because mock.module() cannot
   be cleanly undone mid-suite. Move the mock into the single test that
   needs it and call mock.restore() in a finally block so it is torn
   down as soon as the assertion completes.

2. ExitPlanModeV2Tool.call() was invoked with 2 args but the Tool base
   class signature requires 4 (input, context, canUseTool,
   parentMessage). Pass the two extra args so the test typechecks
   against the real signature.

* fix(test): prevent fs/promises mock leak from ExitPlanModeV2Tool test

Addresses jatmn's P2 on #1725.

The previous test used mock.module('fs/promises', ...) in a try/finally
with mock.restore(). On Linux CI the mock was still active when
loadAgentsDir.test.ts ran, causing all five agent-fixture tests to fail
with "write failed" traces pointing back to this file.

Fix: add afterEach(mock.restore()) to guarantee the mock is torn down
after each test, regardless of pass/fail path. Verified locally by
running both test files in the same process — 6/6 pass.

* fix(test): avoid fs/promises mock entirely to prevent CI leak

The afterEach(mock.restore()) approach still leaked the fs/promises mock
into loadAgentsDir.test.ts on Linux CI (CodeRabbit comment at 12:12 EST).
Bun's mock.module on Linux replaces the module cache in a way that
persists across test files in the same process.

Fix: don't mock fs/promises at all. Instead, re-mock plans.js to point
getPlanFilePath at a nonexistent directory, so the REAL fs/promises.writeFile
rejects with ENOENT. Nothing to leak.

Verified: both test files pass in both orderings with --max-concurrency=1.

* Fix race on exiting plan mode by saving plan file before permission updates and add regression tests asserting no side effects on error

* fix(test): completely isolate mock module by using teammate context APIs instead of module mock override

* test(plan-mode): cover the React write-before-permission guard; drop global fs/promises mock

Addresses jatmn's two P2 findings on #1725.

- Extract the plan-file write guard from ExitPlanModePermissionRequest's
  handleResponse into an exported persistPlanFileBeforeExit() helper (jatmn
  suggested extraction for testability). The component path is unchanged: on
  write failure it stays in plan mode (returns early) and queues a
  'plan-save-error' notification. Adds focused tests for both success and
  write-failure (mutation-checked: a helper that swallows the failure fails the
  test). Tests use real filesystem paths — a temp file for success and a path
  with a missing parent dir for failure — so they need no fs/promises mock.

- ExitPlanModeV2Tool.test.ts: replace the process-global mock.module('fs/promises')
  in both write-failure tests with the same real-failing-path mechanism (a plan
  path whose parent dir does not exist → genuine ENOENT). This removes the
  fragile core-module mock jatmn flagged, which mock.restore() cannot undo and
  which could leak into other test files.

* test(plan-mode): assert the specific ENOENT write failure in ExitPlanModeV2Tool tests

Addresses CodeRabbit's review on #1725: the two write-failure tests asserted a
generic rejects.toThrow(), which would pass on any error. Tighten both to
rejects.toThrow(/ENOENT/) so they lock in the intended missing-parent-dir write
failure rather than masking an unrelated error. The post-write side-effect
assertions (persistFileSnapshotIfRemote / writeToMailbox / setAppState not
called) are unchanged.

* test(plan-mode): add rendered guard test for handleResponse write failure

Addresses jatmn's P3 on #1725: the only coverage for the write-before-permission
guard was at the persistPlanFileBeforeExit helper level; there was no test that
handleResponse itself returns early on write failure.

Renders ExitPlanModePermissionRequest (following the MonitorPermissionRequest
harness), points the V2 plan file at a path whose parent dir is missing (real
ENOENT), confirms the first accept option, and asserts the plan-save-error
notification is queued while toolUseConfirm.onAllow / onReject and onDone are NOT
called. Mutation-checked: removing the `if (!saved) return` guard makes it fail,
so a future refactor cannot silently drop the guard.

* fix(test): type addNotification mock so render test typechecks

CodeRabbit (correctly) flagged that `mock(() => {})` infers a zero-arg tuple, so
`call[0]` was TS2493 and `bun run typecheck` (CI) failed. Type the mock param
with the real `Notification` type, which also lets the assertion drop its cast.
2026-06-25 06:25:58 +08:00
euxaristiaandGitHub adcf5e5839 fix(permissions): bound the speculativeChecks cache with FIFO eviction (#1724)
Internal/upstream defensive maintenance: cap the speculativeChecks Map at
MAX_SPECULATIVE_CHECKS_SIZE (1000) and FIFO-evict the oldest entries after each
insert, so the bash-classifier speculative cache can't grow unbounded when the
classifier path is active (it is a stub in the open-source build; this guards
the upstream build where it is reachable).

Pure cache-bounding — no runtime or permission behavior change, and no new env
vars. Adds focused FIFO-eviction regression tests via a small `_test` surface
(mutation-checked: neutering eviction fails them). Rebased onto current main.
2026-06-25 06:25:00 +08:00
0xfandomandGitHub 3fb718f403 fix(worktree): base agent isolation worktree on parent HEAD, not origin/main (#1652)
* fix(worktree): base agent isolation worktree on parent HEAD, not origin/main

createAgentWorktree delegated to getOrCreateWorktree with no base, so the
non-PR path checked out origin/<defaultBranch> whenever that remote-tracking
ref existed locally. An isolated agent (isolation: "worktree") is expected
to see the same committed state as the parent session, but instead got an
older tree and missed files that exist only on the active branch.

Resolve the parent session's HEAD from the session cwd and thread it through
getOrCreateWorktree as a new `baseRef` option (used verbatim, then rev-parsed
to a SHA). Falls back to the prior origin-based behavior when HEAD can't be
resolved (e.g. a repo with no commits). EnterWorktree/PR worktree paths are
unchanged.

Fixes #1586

* test(worktree): make agent-base regression test hermetic; add explicit cwd seam

The integration test went through createAgentWorktree's ambient getCwd(),
which reads process-global cwd state. bun runs test files concurrently in one
process and a sibling test mutates that global cwd, so the agent-base test
raced and failed in the full suite (worktree based on origin/main, missing
the feature-only file).

Add an optional `cwd` to createAgentWorktree, used for both the canonical
git-root and the parent-HEAD lookups (defaults to getCwd()). The test pins it
explicitly so it no longer depends on the raced global cwd. Verified it passes
isolated and in the full src/utils suite, and still fails on the pre-fix code.

* test(worktree): isolate agent-base regression from leaked module mocks

Per review: the regression test imported createAgentWorktree (and thus
worktree.ts's execFileNoThrow.js dependency) at module load, so it could bind
process-global bun mock.module state left by a neighboring suite before
establishing its own isolation.

Hold the shared mutation lock for the whole test so it never runs interleaved
with suites that mock execFileNoThrow.js, and import the worktree module only
after the lock is held, via a cache-busted dynamic import, so the binding
resolves against the real module.

* test(worktree): use execa and a longer timeout in the agent-base regression

Per AGENTS.md, src/utils tests shell out via execa rather than child_process,
so swap the git() helper to execaSync. The test also performs real git work
(init, commit, branch, worktree add) that can exceed Bun's default 5s timeout
on slower/Windows runners, so give it an explicit 15s budget.

* test: stop two suites leaking partial module mocks process-wide

Bun's mock.module is process-global and is not reverted by mock.restore(), so a
partial-surface mock left active by one suite breaks any later suite that
imports the same module — it sees a module missing the un-mocked exports and
fails to link with "named export not found".

- setupGitHubActions.test.ts mocked config.js with only saveGlobalConfig.
  Spread the real surface (as it already does for execFileNoThrow/browser) and
  restore it in afterEach, so config.js keeps its full surface for later suites.
- osc.test.ts mocked execFileNoThrow.js and tempfile.js with partial stubs and
  never restored them. Spread the real surfaces and gate each overridden export
  on an active-suite flag, so when osc's tests aren't running the exports
  delegate to the real implementation instead of leaking stubs.

This unblocks worktree.agentBase.test.ts (which loads worktree.ts → config.js /
execFileNoThrow.js) when it runs after either suite.

* Revert mock-isolation changes that regressed the full test suite

The previous commit reworked the leaking config.js / execFileNoThrow.js mocks
in setupGitHubActions.test.ts and osc.test.ts (and swapped the worktree test
helper to execa). While it fixed the two-file reproductions, it regressed the
full smoke-and-tests run: createAgentWorktree began failing with "not in a git
repository" in the combined suite. Revert to the last green state while a more
robust isolation approach (process-level isolation for the real-git test) is
worked out.

* test(worktree): run the agent-base regression in an isolated process

The previous in-suite isolation attempts couldn't survive the full test run:
createAgentWorktree shells out to git via execFileNoThrow.js, which other
suites mock with `mock.module` — a process-global override Bun cannot reliably
revert, so any leaked stub made the test fail with "not in a git repository"
depending on suite ordering.

Move the actual createAgentWorktree call into a standalone child process
(worktree.agentBase.fixture.ts) that loads only the real modules, and keep the
git repo setup and assertions in the test using real git directly. Nothing the
shared test process mocks can reach the child, so the test is now order- and
leak-independent. Verified passing in isolation, alongside the two leaking
suites that previously broke it, and in the full suite.

* test(worktree): register agent-base fixture as a knip entrypoint

The agent-base regression runs createAgentWorktree in a standalone child
process (worktree.agentBase.fixture.ts) to escape leaked module mocks. knip
sees no static importer for it — it is spawned via execFileSync — and the
deadcode check fails it as an unused file. It is a genuine process
entrypoint, so add the *.fixture.ts glob to knip's entry list.
2026-06-25 06:23:48 +08:00
0xfandomandGitHub 66ddbece19 fix(bridge): match loopback hostname exactly in HTTPS credential guard (#1760)
* fix(bridge): match loopback hostname exactly in HTTPS credential guard

The bridge requires HTTPS for non-localhost base URLs to protect OAuth
credentials in transit, but the guard decided "localhost" via substring
checks (`baseUrl.includes('localhost')` / `.includes('127.0.0.1')`). Any
remote URL that merely contains that text — `http://evil.example.com/localhost`,
`http://evil.localhost.com`, `http://127.0.0.1.evil.com` — slips past the
check and is allowed to carry credentials over plain HTTP.

Extract `isLocalhostBaseUrl`, which parses the URL and matches the hostname
component exactly (mirroring the logic already in `buildSdkUrl`), and route
both guard sites plus `buildSdkUrl` through it. A malformed URL is treated as
non-local so the HTTPS requirement still applies.

Add direct coverage for the helper, including the substring-bypass cases.

* fix(bridge): reject mixed-case HTTP scheme in credential guard

The HTTPS credential guard tested `baseUrl.startsWith('http://')`, which only
matches the exact lowercase scheme. `HTTP://example.com` / `Http://example.com`
slipped past — the URL parser normalizes the scheme to `http:`, so credentials
would still be sent over plaintext.

Fold the scheme test into the parse: add `isInsecureHttpBaseUrl`, which reads
`new URL(...).protocol === 'http:'` (case-normalized) and requires a non-local
host, and route both guard sites through it. Malformed URLs return false.

Extend coverage with mixed-case HTTP/HTTPS cases.
2026-06-24 22:04:05 +08:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
39604fe871 chore(main): release 0.20.0 (#1684)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
v0.20.0
2026-06-24 15:07:30 +08:00