1189 Commits
Author SHA1 Message Date
7674d4d73e feat(aimlapi): provider foundation (1/5) — config, catalog, ambient-key gate (#1995)
* feat(aimlapi): provider foundation — config, catalog, ambient-key gate

First layer of the AI/ML API onboarding split. Self-contained: config
endpoints + partner-header resolution, gateway catalog entry, runtime
metadata and artifact-generator wiring, and the regenerated integration
manifest.

Also lands the P1 security fix in providerProfiles: an ambient AIMLAPI
key is only forwarded when the profile targets the canonical inference
endpoint, so a proxy/staging profile can never leak the key elsewhere.

No client/checkout/UI changes here — those stack in later PRs.

* fix(aimlapi): harden credential gating and restore renamed-preset test path

* fix(aimlapi): harden credential gating and finish renamed-preset path

* fix(aimlapi): finish aimlapi.com rename in provider UI; gate generic proxy credential

* fix(aimlapi): close remaining proxy credential and attribution leaks

* fix(aimlapi): withhold ambient credentials and attribution from proxies

* fix(aimlapi): withhold ambient credentials and attribution from proxies

* fix(aimlapi): stop forcing profile credentials onto retargeted endpoints

* fix(aimlapi): withhold ambient custom headers from proxy launches

---------

Co-authored-by: Lookoff123 <bataryshkinairina@gmail.com>
2026-07-20 10:02:00 +08:00
5399a11d3c fix(openai): compress tool history on Responses requests (#1958)
* fix(openai): compress tool history on Responses requests

* fix(openai): tighten transport preprocessing

* fix(openai): preserve structured compressed tool results

* fix(openai): tier parallel tool results independently

* fix(openai): preserve Ollama image endpoint fallback

* fix(openai): omit old inline image payloads

* fix(openai): bound inline image history

* fix(openai): preserve local image fallback

* fix(openai): bound data URL tool images

* fix(openai): match parameterized image data URLs

* fix(openai): bound adjacent tool images

* fix(openai): bound leading tool images

* fix(openai): size structured history per transport

* fix(openai): scope attached image compression

* fix(codex): match tool text serialization budget

* fix(openai): recompress GitHub Responses fallback

* fix(openai): preserve user images in history compression

* fix(openai): scope history image omission to result ownership

* fix(messages): preserve image owners after media retry

* fix(messages): retain image ownership through normalization

---------

Co-authored-by: jatmn <the@jat.mn>
2026-07-20 09:53:20 +08:00
JATMNandGitHub 1ddb7d6839 test: isolate PowerShell governance policy settings (#2000) 2026-07-19 09:24:45 +08:00
JATMNandGitHub 630fc2d980 fix(compaction): honor disabled auto-compact under memory pressure (#1999)
* fix(compaction): honor disabled auto-compact under memory pressure

* fix(compaction): preserve overflow recovery

* test(compaction): cover disabled config setting
2026-07-19 09:24:31 +08:00
Xiang HanandGitHub 86fb6db85c fix(session-title): ignore API error responses (#1992) 2026-07-19 09:23:58 +08:00
3808d19da4 fix(api): enforce API_TIMEOUT_MS for OpenAI-compatible headers (#1940)
* fix(api): enforce API_TIMEOUT_MS for OpenAI-compatible headers

* test(api): cover Copilot responses fallback deadlines

* fix(api): redact secrets in timeout URL paths

* fix(api): harden Copilot response deadlines

* fix(api): prevent header-timeout request replay

* fix(api): harden timeout cleanup and redaction

* fix(api): redact encoded transport credentials

* fix(api): harden deadline retries and URL redaction

* fix(api): preserve aborted fetch reasons

* fix(api): preserve caller abort reasons

* test(api): clear caller abort timer

* docs(api): clarify API_TIMEOUT_MS transport scope

* docs(api): explain timeout env loading

* fix(api): reset deadline for proxy retries

* fix(api): type deadline fetch adapter

* fix(api): honor abort cleanup and request signals

* fix(api): do not block proxy retries on body cancellation

---------

Co-authored-by: jatmn <the@jat.mn>
2026-07-19 09:21:14 +08:00
eb72c770c4 fix(repl): add correction context after interruption (#1936)
* fix(repl): add correction context after interruption

* test(repl): exercise interruption correction lifecycle

* refactor(repl): bind correction tracker to query guard

* fix(repl): retain initialized correction tracker

* fix(repl): track pre-query interruption corrections

* fix(repl): exclude remote turns from correction context

* fix(repl): harden correction reminder lifecycle

* fix(repl): keep correction reminder request-scoped

* test(query): cover absent request-only context

* fix(repl): clear correction context on compaction

* fix(repl): preserve interruption correction recovery

* fix(repl): clear stale correction state on boundary removal

* fix(messages): recover merged attachment retries

* fix(messages): recover attachment retries safely

* fix(messages): recover ambiguous attachment retries

* fix(repl): clear stale correction reminders

* fix(query): clear correction context after reactive compaction

* fix(repl): declare restore callback dependencies

* fix(repl): preserve correction state across rewrites

* fix(repl): scope correction tracking to model calls

* test(repl): remove structural lifecycle assertions

* fix(repl): preserve interruption correction reminders

* fix(repl): retain reminder before queued dispatch

* fix(repl): retain correction reminder until model dispatch

* fix(repl): preserve reminders for queued corrections

* fix(repl): arm correction only at provider dispatch

* fix(repl): scope interruption correction to model requests

* fix(repl): retain reminders until model dispatch

* fix(repl): ignore queued slash commands on interrupt

* fix(messages): strip rejected tool-result media

* fix(messages): apply nested media retry stripping

* fix(messages): preserve empty tool results after media retry

* fix(repl): preserve correction and media retries

* fix(repl): retain correction through tool execution

---------

Co-authored-by: jatmn <the@jat.mn>
2026-07-18 22:48:00 +08:00
de76950f60 feat(provider): route GPT-5.6 models to the OpenAI Responses API (#1961)
* feat(provider): route GPT-5.6 models to the OpenAI Responses API

GPT-5.4/5.5/5.6 (incl. gpt-5.6-sol/terra/luna) reject function tools +
reasoning_effort on /v1/chat/completions, so an agent CLI (which always
sends tools) can't use them. Add a model+base predicate
(modelRequiresResponsesApi) that auto-selects the existing /v1/responses
transport for these models on api.openai.com and Azure OpenAI hosts.
Precedence: explicit responses/responses_compat > catalog
requiredApiFormat > explicit chat_completions > predicate > default. The
gpt-5.6 catalog entries deliberately set no requiredApiFormat so the
chat_completions escape hatch works for them. Register the gpt-5.6
descriptors and openai-vendor catalog entries (with reasoning metadata so
buildResponsesBody emits nested reasoning.effort).

Also fix a latent bug: the responses branch of buildRequestUrl emitted a
bare ${base}/responses and skipped Azure handling, so a forced/auto
responses route 404'd on Azure. It now mirrors buildChatCompletionsUrl —
deployment-style bases get the deployment path + api-version, bases
already containing /deployments/ keep their path and gain api-version,
while the modern Azure v1 surface (.../openai/v1) is preserved as
${base}/responses.

* fix(provider): honor OPENAI_AZURE_STYLE in the responses gate and use the Azure v1 responses surface

CodeRabbit review on #1961: the responses auto-route gate only checked
hostnames, ignoring the OPENAI_AZURE_STYLE override the shim honors for
custom/private Azure endpoints (APIM-fronted, private link). The Azure
detection is now a single shared predicate (isAzureStyleBaseUrl) used by
both the gate and the shim: OPENAI_AZURE_STYLE truthiness first, then
hostname matching.

Per Microsoft's docs, the Responses API exists only on the Azure v1
surface ({resource}/openai/v1/responses, model in the request body, no
api-version, no deployment-scoped form), so buildResponsesUrl now
normalizes any Azure-style base to that surface instead of mirroring the
chat builder's deployment-path + api-version form, which built endpoints
that do not exist.
https://learn.microsoft.com/en-us/azure/foundry/openai/how-to/responses

* fix(provider): address maintainer review on GPT-5.6 responses routing

Narrows the responses auto-route to verified variants (gpt-5.4/5.5/5.6
minus -mini/-nano, two-digit minors deliberately unmatched), corrects the
gpt-5.6 context window to 1,050,000 per the OpenAI model pages, documents
OPENAI_AZURE_STYLE's routing effect in .env.example, and hardens
buildResponsesUrl normalization (trailing-slash strip, stacked Azure
suffixes stripped until stable). Also pins --max-concurrency=1 on the
default test script and adds direct coverage for isAzureStyleBaseUrl, the
override-driven responses URL, and the gpt-5.6 catalog metadata.

* test(provider): pin responses predicate behavior for patch and suffixed ids

Pins gpt-5.4.1 (patch of a verified family, routed), gpt-5.41 (two-digit
minor read, not routed), and gpt-5.6-mini-high (mini variant, not routed)
so the predicate's edge behavior is asserted rather than implied.

* fix(provider): responses-contract test, regional OpenAI hosts, Azure deployment docs, env allowlist

Updates the providerOverride gpt-5.4 effort test to the Responses contract
the auto-route now sends (nested reasoning.effort, /responses URL); widens
the auto-route host check to OpenAI-controlled *.api.openai.com regional
endpoints; documents and regression-tests the explicit
OPENAI_API_FORMAT=responses path for arbitrary Azure deployment names; and
allows OPENAI_AZURE_STYLE through the --provider-env-file allowlist.

* fix(provider): narrow responses auto-route to verified minors and carry GPT-5.6 reasoning metadata on Azure

Narrows the model-name auto-route predicate from gpt-5.[4-9] to gpt-5.[4-6]
so unverified future minors (5.7/5.8/5.9) are not auto-routed, and syncs the
comment plus the two remaining "5.4+" phrasings in .env.example.

Fixes GPT-5.6 reasoning metadata on Azure and regional OpenAI bases: those
hosts resolve to route 'custom' (empty catalog), so resolveCatalogReasoningMetadata
returned undefined and the request dropped its default 'high' effort and the
reasoning.encrypted_content include. It now falls back to the openai vendor
catalog by model name on route 'custom', so gpt-5.6 carries its advertised
default 'high' and xhigh instead of incidental legacy controls.

* fix(provider): gate the custom-route reasoning fallback to verified OpenAI/Azure bases

The round-3 custom-route fallback also fired for arbitrary OpenAI-compatible
gateways (which resolve to route 'custom' too), injecting a default
reasoning_effort:high on a chat_completions request those gateways may reject
— a behavior change on third-party gateways the PR promised not to make.
Gate the fallback on baseUrlSupportsResponsesAutoRoute (the same verified
OpenAI/Azure surfaces the Responses auto-route uses), threading the request
base via the reasoning context (process.env fallback for the upstream path).

* test(provider): isolate OPENAI_API_BASE/OPENAI_AZURE_STYLE in the gpt-5.6 reasoning tests

The Azure/regional/gateway reasoning tests snapshot-restored only
CLAUDE_CODE_USE_OPENAI/OPENAI_BASE_URL/OPENAI_API_KEY. A leaked
OPENAI_AZURE_STYLE from another test would make isAzureStyleBaseUrl treat
the gateway base as Azure-style, firing the fallback and flipping the
'no injected default' assertion. Snapshot both keys and delete them before
each test's setup so a leaked value cannot corrupt the result.

* fix: preserve GPT-5.6 fallback and Azure routing

* fix: cover GPT-5.6 Azure edge cases

* fix: cover GPT-5 forced chat tools

* fix(provider): narrow Azure-style responses routing

* fix(provider): isolate agent overrides from Azure mode

* fix(provider): isolate override reasoning from Azure mode

* fix(provider): isolate override API format

* fix(provider): preserve responses effort routing

* fix(provider): restore safe context and clear Azure mode

* fix(provider): preserve Azure routing state

* fix(provider): preserve Azure profile routing

* fix(provider): preserve automatic Responses routing in profiles

---------

Co-authored-by: jatmn <the@jat.mn>
2026-07-18 22:47:24 +08:00
0effa0f42b test: seal mock.module leaks in awaySummary and diff smoke tests (#1981)
* test: restore compact test module stubs

* test: restore remaining compact mock modules

* test: isolate auto compact from compact mocks

* test: seal mock.module leaks in awaySummary and diff smoke tests

awaySummary.test.ts stubbed ./api/claude.js and
./SessionMemory/sessionMemoryUtils.js, and diff.test.ts stubbed
src/services/analytics/index.js, each with an incomplete module that was
never restored. bun evaluates every test file's module-level imports up front
and shares one process across the whole suite, so these stubs leaked into all
downstream importers (claude.js: ~22, analytics/index.js: ~245), producing
order-dependent failures in the smoke run.

Register the stubs in beforeAll (not at module load) and unmock() in
afterAll, and acquire sharedMutationLock so the real modules are already
cached for every other file at startup. Verified: a downstream importer now
sees the real queryModelWithStreaming / logEventAsync, and the full suite
failure count drops 73 -> 70.

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

* test: restore module mocks under shared lock

* test: guard mock restoration against lock races

* test: retain lock ownership through compact cleanup

* test: retain lock ownership through auto-compact cleanup

* test: restore mocks after setup failures

* test: isolate compact task output cleanup

* test: preserve full compact mock restoration

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-17 12:37:39 +08:00
JATMNandGitHub 47123b4b38 test: restore global mock isolation (#1980) 2026-07-17 00:07:56 +08:00
JATMNandGitHub 2516eef039 test: restore compact mock isolation (#1979)
* test: restore compact test module stubs

* test: restore remaining compact mock modules

* test: isolate auto compact from compact mocks
2026-07-17 00:07:25 +08:00
JATMNandGitHub 7ad96e917e test: isolate flaky smoke assertions (#1978) 2026-07-16 10:17:17 +08:00
BogdanandGitHub 507ba4b804 perf(tools): preserve UTF-8-safe head and tail in persisted previews (#1960)
* perf(tools): preserve UTF-8-safe head and tail in persisted previews

* fix(tools): derive persisted preview size from file

* fix(tools): report exact persisted preview bytes

* fix(tools): bound and sanitize persisted previews

* fix(tools): reconcile sanitized preview metadata
2026-07-16 08:19:41 +08:00
JATMNandGitHub 487cae7185 fix(api): self-heal tool_stream rejection from non-Z.AI gateways (#1950) (#1951)
* fix(api): self-heal `tool_stream` rejection from non-Z.AI gateways (#1950)

GLM-5.2 served through NVIDIA NIM (`integrate.api.nvidia.com`) is
rejected with `400 Unsupported parameter(s): tool_stream` because
`tool_stream` is a Z.AI-proprietary streaming extension.

Changes:
- Add a `tool_stream_unsupported` OpenAI-compatibility failure category
  that detects the `tool_stream` rejection (the existing
  `tool_call_incompatible` matcher only matches `tool_call`, not
  `tool_stream`).
- Self-heal in the OpenAI shim: when a gateway rejects `tool_stream`, drop
  only that parameter and retry with tools intact (streaming tool calls
  simply aren't streamed on such gateways). This is a defensive net for any
  provider that slips the parameter through, alongside the existing
  catalog/runtime gating that suppresses it for non-Z.AI routes.
- Give remote requests one self-heal attempt budget so the retry can run.
- Surface a friendly assistant message for the new category.

Tests:
- Regression test: NVIDIA NIM GLM streaming+tools never sends `tool_stream`.
- Regression test: shim self-heals a `tool_stream` 400 by retrying without it.
- Classification tests for `tool_stream_unsupported`.
- Runtime metadata regression test for NVIDIA NIM GLM-5.2 (no `tool_stream`,
  reasoning shim preserved).

* fix(shim): scope tool stream retry budget

* fix(api): clarify tool stream fallback error

* fix(api): prioritize tool stream rejection

* fix(api): match unrecognized tool_stream arguments

* fix(api): harden tool stream self-heal

* fix(api): accept quoted tool stream errors

* fix(api): match tool stream parameter variants

* fix(api): recognize structured tool stream errors

* fix(api): avoid tool stream name false positives

* fix(api): classify gateway tool stream rejections

* fix(api): exclude tool name validation errors

* fix(api): handle structured tool stream validation

* fix(api): harden tool stream error classification

* fix(api): harden tool stream error classification

* fix(api): narrow tool stream recovery classifier

* fix(api): support FastAPI tool stream errors

* fix(api): accept wrapped tool stream errors

* fix(api): harden tool stream recovery classifier

* fix(api): harden tool stream recovery classifier

* fix(api): distinguish tool stream schema diagnostics

* fix(api): handle structured tool stream errors

* fix(api): avoid tool stream schema false positives

* test(api): isolate watchdog client mock

* test(api): restore watchdog client mock

* test(api): reset watchdog module mock

* test(api): scope watchdog client stub
2026-07-16 08:18:33 +08:00
BogdanandGitHub 1f20e92c2e fix(permissions): enforce read-only plan mode (#1938)
* fix(permissions): enforce read-only plan mode

* fix(permissions): narrow hook approval types

* fix(permissions): harden plan-mode decision boundaries

* fix(permissions): close plan-mode hook races

* test(permissions): clarify platform path case coverage

* fix(permissions): guard permission hook rewrites

* fix(permissions): close remaining plan-mode escapes

* test(speculation): satisfy strict context typing

* fix(permissions): close hook update race windows

* fix(permissions): close plan approval races

* fix(permissions): align prompt decision type

* fix(permissions): close plan transition races
2026-07-16 08:15:43 +08:00
JATMNandGitHub 46e80568be fix(provider): support custom Anthropic bearer auth (#1929)
* fix(provider): support custom Anthropic bearer auth

* feat(provider): add custom Anthropic profile flow

* fix(provider): restore custom Anthropic tokens on startup

* fix(provider): preserve custom Anthropic env setup

* fix(provider): clear stale custom Anthropic tokens

* fix(provider): preserve custom Anthropic API-key env setup

* fix(provider): cover custom Anthropic auth routing

* test(api): isolate custom Anthropic client routing

* test(api): cache-bust client provider imports

* feat(provider): clarify custom provider presets

* fix(provider): address custom Anthropic review feedback

* fix(provider): preserve custom Anthropic headers

* fix(provider): require custom Anthropic token

* fix(provider): isolate custom Anthropic credentials

* fix(provider): guard custom Anthropic setup

* fix(provider): complete custom Anthropic integration

* fix(provider): classify custom Anthropic proxies

* fix(provider): gate proxy cache extensions

* fix(provider): preserve custom Anthropic isolation

* fix(provider): retain direct proxy model option

* fix(provider): honor custom endpoint boundaries

* fix(provider): keep proxy credentials local

* fix(provider): disable proxy fast mode

* fix(provider): preserve first-party route identity

* fix(provider): isolate custom Anthropic endpoints

* fix(provider): gate remaining first-party features

* fix(provider): isolate custom Anthropic proxy features

* test(web-search): make Brave timeout mock abort-aware

* fix(provider): address custom Anthropic review feedback

* test(provider): cover first-party beta gates

* fix(provider): complete custom Anthropic isolation

* fix(provider): complete custom Anthropic routing

* fix(provider): address custom Anthropic review followups

* fix(provider): close custom Anthropic review gaps

* test(provider): keep custom Anthropic mock helpers isolated

* test(provider): isolate model options gateway mocks

* fix(provider): stabilize custom Anthropic model option display

* fix(provider): address remaining review threads

* fix(provider): synchronize active profile persistence

* fix(provider): preserve custom Anthropic API key auth

* fix(provider): avoid forwarding inherited Anthropic keys

* fix(provider): guard custom auth selection

* fix(provider): require first-party Anthropic port

* test(web-search): avoid duplicate shared lock

* fix(provider): resolve remaining review findings

* fix(provider): harden custom Anthropic routing

* fix(provider): simplify Anthropic thinking gate

* fix(provider): preserve custom proxy routing and secret permissions

* fix(mcp): isolate Claude.ai config cache by provider

* fix(model): keep custom endpoints out of first-party UX

* fix(provider): scope Opus off switch to Anthropic

* fix(provider): disable tool search for custom proxies

* fix(provider): close custom Anthropic review gaps

* fix(provider): reject Anthropic staging custom profiles

* fix(webfetch): classify custom Anthropic endpoints

* fix(provider): block bearer auth at Anthropic origin

* fix(provider): keep custom auth off staging OAuth

* test(provider): strengthen auth regression coverage
2026-07-16 08:14:22 +08:00
626c4873ab feat(statusline): show token counts in context bar (ctx 74K/200K (37%)) (#1967)
* feat(statusline): show token counts in context bar (ctx 74K/200K (37%))

Instead of just percentage, show actual token usage and context window
size with integer K/M prefixes (uppercase, en-US locale).

- BuiltinStatusLine: added contextInputTokens + contextWindow fields
- buildBuiltinStatusSegments: formats "ctx {used}/{window} ({pct}%)"
- format.ts: added formatTokenCount() — integer, uppercase K/M
- All 18 tests pass

* fix(statusline): call getCurrentUsage once and reuse result

* fix(statusline): mark estimated token counts with ~ prefix

When getCurrentUsage() returns is_estimated:true (provider reported
all-zero usage), show "ctx ~74K/200K (37%)" instead of "ctx 74K/200K
(37%)" so built-in statusline preserves the estimate distinction
exposed by the custom-statusline contract.

* fix(openai-shim): send stream_options for non-Ollama local providers

Previously stream_options was disabled for all local URLs (127.0.0.1,
192.168.x.x, etc.), preventing llama-server and other self-hosted
OpenAI-compatible servers from returning usage in SSE streams. Now only
Ollama (localhost:11434) is excluded, since it rejects stream_options.
All other providers including llama-server receive stream_options and
their prompt_tokens/completion_tokens are correctly mapped via
buildAnthropicUsageFromRawUsage.

---------

Co-authored-by: Andrey Bezborodov <andrey@getdataflow.ru>
2026-07-15 23:58:44 +08:00
a32781537f fix(query): bound per-turn latency growth in long REPL sessions (#1949) (#1952)
* fix(query): bound per-turn latency growth in long REPL sessions (#1949)

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

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

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

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

* fix(query): repair REPL latency guard

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

* fix(query): enforce cap across interactive paths

* docs(compaction): clarify disabled message limits

* fix(query): retain explicit message thresholds

* fix(query): enforce explicit threshold recovery

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

* fix(doctor): report effective message compaction limit

* fix(config): share message threshold validation

* test(doctor): cover disabled message compaction

* fix(compact): preserve latency guard coverage

* test(repl): exercise turn cap defaults

* fix(compact): honor disabled default message guard

* fix(swarm): honor disabled auto compaction

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-15 23:06:53 +08:00
0xfandomandGitHub 1d053b2a3d fix(resume): read the session tag from its own entry, not a tool's tag input (#1975)
readLiteMetadata scanned the raw 64KB tail for "tag":"..." without scoping to
the {"type":"tag"} entry, so any nested occurrence matched too. A tool_use input
carrying a tag parameter (Docker image tags, git tags, cloud resource tags) is
stored as literal JSON inside an assistant entry appended after the tag entry,
and last-occurrence wins — so the tool's value was reported as the session tag.
In /resume that surfaces a phantom tag tab and misfiles the session away from
its real one; an untagged session could also acquire a tag it never had.

The other two tag readers already type-scope for exactly this reason
(listSessionsImpl.ts:132, sessionStorage.ts:782, both with comments naming the
tool_use collision), and sessionBranch in this same function is scoped via
SESSION_BRANCH_ENTRY_PREFIX. This reader just missed it; mirror them.
2026-07-15 22:57:25 +08:00
14648213a6 Chore/readme cleanup (#1976)
* docs: README cleanup, green wordmark header, Trendshift badges

Header: the startup wordmark (src/constants/brand.ts half-block art)
rendered as a green two-shade SVG (docs/assets/openclaude-wordmark.svg,
textLength-pinned so rows align in any monospace font), with the three
Trendshift badges (daily/monthly/repository) centered beneath it.

Cleanup (536 -> ~430 lines, nothing lost):
- Agent routing, maxSteps limits, and GitHub Copilot sub-agent tuning
  moved to docs/agent-routing.md; headless gRPC server moved to
  docs/grpc-server.md; README keeps linked summaries.
- Build/test/validation commands were repeated in three sections —
  consolidated into one Development section; Contributing links to it.
- New "Meet Your Buddy" section documenting the companion heroes and
  their /buddy commands; added to What Works and Why OpenClaude.
- Star History moved from the header flow down beside Community.
- Setup Guides indexes the new docs pages; fixed a missing blank line
  before Repository Structure and a curly quote.

All relative links, image paths, and internal anchors validated.

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

* docs: pure-rect wordmark for crisp rendering; drop broken Star History

The wordmark SVG previously drew the half-block art as monospace <text>,
which rendered raggedly (font-dependent glyph stretching and seams).
Regenerated as pure SVG rects computed from the brand.ts wordmark grid —
no font dependence, pixel-crisp at any size, same two-shade green split.

Star History chart removed: the badge endpoint errors and displays a
broken image.

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

* docs: render the wordmark at full README column width

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

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-15 22:27:50 +08:00
d683e85395 feat(buddy): hero pixel-art companions with signature Enter animations (#1972)
* feat(buddy): hero pixel-art companions with signature Enter animations

Rebuild the buddy system as heroes-only. The 18 legacy rolled species are
removed; the hatch pool is now 7 hero forms — robinhood, kaio, strawhat,
merlin, kage, ember, corsair — each hand-pickable via /buddy set.

Every hero has 22x16 truecolor half-block pixel art (idle + action poses),
a line-art fallback for low-color terminals, a narrow-mode face, and a
signature effect that fires on every message submission: arrow with impact
thunk, charging full-width energy wave, stretchy punch that extends and
snaps back, twinkling sparkle stream, spinning shuriken, gradient fire
cone, and cannonball with smoke trail.

Engine: companion animation moves from a raw 500ms setInterval to the
shared animation clock (useAnimationFrame; pauses when hidden, respects
prefersReducedMotion), with a one-shot 50ms burst driver (useShotClock,
arm-then-anchor to avoid stale-tick draw-phase skips) and a general
ActionEffect system (pure draw/travel/impact functions, frame-tested).
Effects travel right-to-left toward the prompt — matching where the
sprite actually stands.

Commands: /buddy set <form|random>, /buddy name <name>, muted-buddy
feedback (silent no-op pets now explain themselves), and a hatch-message
fix so the announced species always matches the displayed sprite (the
message previously rolled with a different seed).

BREAKING: existing rolled pets transform into a hero on upgrade (name and
personality persist; speciesOverride pins are unaffected).

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

* fix(buddy): address CodeRabbit review on PR #1972

- useShotClock: consume an in-flight shot when playback becomes
  ineligible mid-flight (mute/reduced-motion/resize), so re-enabling
  can't resume a stale animation.
- /buddy unmute: emit a greeting reaction — the sprite reads
  companionMuted non-reactively and its clock is paused while hidden,
  so a config-only unmute left it invisible until an unrelated
  re-render.
- /buddy name: strip ANSI escapes and control/format characters before
  saving, and cap by display width (stringWidth) instead of UTF-16
  length.
- CompanionSprite: track bubble age in sync-render state instead of an
  effect-updated ref, so a fresh reaction can't render pre-faded.
- companion_intro already keyed on name+species (prior commit); tests
  now pin exact faces for all seven heroes, separate idle/shoot pixel
  frame counts, and decode-guard the charCode species constants.
- CompanionActionFX tests: deterministic companion fixture via
  complete-config module mock; raw (untrimmed) output compared against
  a rendered-null baseline so a spurious blank FX row fails.
- companion.test: re-register the real config module in afterAll
  (mock.restore does not undo mock.module).
- Types: SPECIES_COLORS and FORM_FLAVOR are full Records (compile
  error on a colorless/flavorless future hero); dead RARITY_COLORS
  removed.

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

* test(buddy): regression coverage for bubble age reset on reaction change

Renders CompanionSprite against a fake shared clock: ages the first
bubble past the fade threshold, swaps the reaction WITHOUT advancing
the clock, and asserts the fresh bubble renders unfaded. Fading is
detected structurally (border and text collapse to one color when
fading) so the test is independent of the active theme's exact values.
Requested by CodeRabbit on PR #1972.

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

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-15 21:50:10 +08:00
0xfandomandGitHub 7b9e477519 fix(memdir): enforce entrypoint cap in bytes, not UTF-16 char length (#1918)
* fix(memdir): enforce entrypoint cap in bytes, not UTF-16 char length

truncateEntrypointContent bounds MEMORY.md/CLAUDE.md content with
MAX_ENTRYPOINT_BYTES (the field is byteCount, the flag wasByteTruncated,
and the warning renders it via formatFileSize) but measured and cut with
String.length, which counts UTF-16 code units. For multibyte content
(CJK/emoji at 3-4 bytes each) that undercounts by up to ~4x, so a file
that is tens of KB of real bytes but under 25,000 chars slips past the cap
entirely — reported as a fraction of its true size and passed through
uncapped, defeating the budget whose purpose is to bound context bloat.

Measure with Buffer.byteLength and perform the newline-boundary cut in byte
space (Buffer.lastIndexOf(0x0a) + subarray) so the cap actually bounds
bytes. Add regression coverage for a multibyte file over the byte cap but
under the char cap, and for small multibyte content left untouched.

* fix(memdir): keep the hard byte cut on a UTF-8 boundary

The no-newline fallback in truncateEntrypointContent hard-cut the UTF-8
buffer at MAX_ENTRYPOINT_BYTES. If that offset landed inside a multibyte
character, decoding with toString('utf8') emitted a U+FFFD replacement char,
so the body was no longer bounded by the cap — e.g. a single 30KB line of
CJK produced a 25,002-byte pre-warning body against a 25,000 cap.

Back up to the character's first byte (skip continuation bytes 0b10xxxxxx)
before slicing so the decoded body stays within the cap and contains no
replacement chars. Add a regression for the no-newline CJK case.

* docs(memdir): tell memory writers about the entrypoint byte cap

The save instructions only warned that MEMORY.md is truncated after 200
lines. Now that the 25KB byte cap is effective for multibyte content, a user
can stay well under 200 short CJK entries and still lose the tail of the
index without the writer knowing the byte cap exists. Update both the
private-memory guidance (buildMemoryLines) and the team-memory guidance
(teamMemPrompts) to state both limits — 200 lines or ~24KB, whichever comes
first.

* docs(memdir): state the byte cap in the extraction writer prompts

The background auto-only and combined memory extractors load MEMORY.md through
truncateEntrypointContent, which this branch makes enforce the 25KB byte cap for
multibyte content. Their save instructions still told the model only that lines
after 200 are truncated, so an extractor could satisfy the prompt with fewer
than 200 long entries and still lose the index tail on the next load. State both
the line and byte limits, matching the interactive private/team guidance.

* test(memdir): cover the combined line + byte truncation path

Add a case where content exceeds both MAX_ENTRYPOINT_LINES and
MAX_ENTRYPOINT_BYTES, so line truncation runs first and the byte cut then
applies to the already line-clipped result. Asserts the combined reason string,
the byte-bounded body, and no split-character replacement chars.
2026-07-15 20:51:55 +08:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2ff93a10bf chore(main): release 0.24.0 (#1883)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
v0.24.0
2026-07-14 22:18:45 +08:00
0xfandomandGitHub 9fc8806f8d fix(cache): use a monotonic clock for conversation-cache LRU recency (#1965)
evictLRU picks the entry with the smallest recorded access order, but that
order was set to accessOrder.size — which plateaus at ~maxSize once the cache is
full and drops on delete, so it is not monotonic. After the cache saturates,
every set/get stamps roughly the same value, and a freshly-inserted key can end
up ranked below entries a prior get already bumped. The next insert then evicts
the most-recently-touched key instead of the least-recently-used one, churning
the 50-entry session-history cache (sessionHistory.ts) into needless refetches.
Stamp recency from a monotonic counter that only ever increases.
2026-07-14 22:10:40 +08:00
Xiang HanandGitHub f961ae7432 fix: suspend footer work during ctrl-c feedback (#1963) 2026-07-14 22:07:31 +08:00
0xfandomandGitHub ed3927d0f5 fix(editor): account for NFC boundary composition in insert offset (#1954)
Cursor.modifyText builds newText from prefix + insert + suffix and hands it to
Cursor.fromText, which NFC-normalizes the whole string. But the new cursor
offset was computed as startOffset + insertString.normalize('NFC').length,
normalizing the insert in isolation. When the inserted text begins with a
combining mark that composes with the last character of the prefix (e.g. "e" +
U+0301 -> "é"), the normalized newText is one UTF-16 unit shorter than that
formula assumes, so the returned offset overshoots by one and the cursor lands
past the following text — the next keystroke then edits the wrong spot.

Measure the normalized prefix-plus-insert instead, so cross-boundary
composition is accounted for. Reduces to the previous behavior whenever no
boundary composition occurs (plain ASCII, astral emoji, insert at start).
2026-07-14 14:43:40 +08:00
c11c88bd91 perf: bound file IO concurrency (#1948)
* perf: bound file IO concurrency

* fix: honor abort and failure gates in bounded IO

* fix: preserve attachment timeout behavior

---------

Co-authored-by: Gautam Manchandani <gautammanch@Gautams-MacBook-Air.local>
2026-07-14 14:42:25 +08:00
BogdanandGitHub af0885d8ec fix(bg): revalidate process identity before signals (#1937)
* fix(bg): revalidate process identity before signals

* fix(bg): sanitize process identity probe failures

* fix(bg): skip signals for terminal sessions
2026-07-14 14:40:32 +08:00
JATMNandGitHub 6b2a1d8aef test(websearch): allow shared timeout test lock wait (#1959) 2026-07-14 14:39:00 +08:00
CalandGitHub 2da3e4c400 docs: clarify pre-PR contributor checklist (#1920)
* docs: clarify pre-PR contributor checklist

* docs: require contribution guide confirmation
2026-07-14 11:07:46 +08:00
JATMNandGitHub 0dc622e129 test(cli): clean skills temp directories (#1946)
* test(cli): clean skills temp directories

* test(cli): handle stream cancellation errors
2026-07-13 16:21:05 +08:00
BogdanandGitHub 85cf2ac55a fix(update): avoid upstream package commands for custom builds (#1944)
* fix(update): avoid upstream package commands for custom builds

* test(update): cover runtime package identity
2026-07-13 16:20:30 +08:00
0xfandomandGitHub 2970b5fd5f fix(context): order pruned messages by envelope timestamp, not phantom field (#1934)
pruneByRelevance keyed recency scoring, the group tie-break, and the final
"restore chronological order" sort off message.message?.created_at. That
nested API-body field is never populated on our Message objects (nothing in
the tree assigns it), so every read was undefined and `?? 0` made all three
into no-ops: the recency bonus never fired, the tie-break never broke ties,
and the final sort left the list as [...recentMessages, ...olderGroups] —
the newest preserveRecent messages jumped ahead of older retained ones.

The chronological key actually lives on the Message envelope as `timestamp`
(an ISO-8601 string present on every variant). Add a messageTimeMs() helper
that parses it and route all three sites through it. This runs in the
auto-compaction path (autoCompact -> pruneByRelevance), so the reordered
list was being sent to the model. Add a regression using the real envelope
shape asserting retained messages stay in chronological order.
2026-07-13 16:19:33 +08:00
0xfandomandGitHub 9bf9926805 fix(mcp): preserve ":-" inside ${VAR:-default} default values (#1933)
expandEnvVarsInString split the ${VAR:-default} syntax with
varContent.split(':-', 2). The code comment says the limit is there to
"preserve :- in defaults", but JavaScript's String.split(sep, limit) caps
the array length and discards the remainder — it is not a maxsplit that
glues the tail back on. So a default that itself contains ':-' is
truncated at the first occurrence: ${VAR:-a:-b} expands to "a" instead of
bash's "a:-b".

Slice at the first ':-' with indexOf so any later ':-' stays in the
default. This runs over user .mcp.json command/args/env/url/headers values.
Add coverage for the ':-'-in-default case plus set/unset/empty-default and
missing-var paths.
2026-07-13 16:18:57 +08:00
JATMNandGitHub 2448ea9bfe fix(query): warn before repeated tool failures stop (#1927)
* fix(query): warn before repeated tool failures stop

* fix(query): preserve tool failure advisories

* fix(query): forward all tool failure advisories

* fix(query): harden tool failure advisories

* fix(query): preserve tool failure advisories

* fix(query): keep advisories one-shot

* fix(query): avoid duplicate advisories after compaction

* fix(query): compare advisory message IDs

* fix(query): cover advisory forwarding edges

* fix(query): retain advisories without tools
2026-07-12 22:27:48 +08:00
0xfandomandGitHub ffc5a6e8fc fix(gitdiff): apply the 1MB diff cap in bytes, not UTF-16 char length (#1932)
parseGitDiff skips per-file diffs larger than MAX_DIFF_SIZE_BYTES (1 MB) —
the constant is named _BYTES, its comment says "1 MB", and the function's
doc promises "Files >1MB: skipped entirely". But the guard measured
fileDiff.length, i.e. UTF-16 code units. Multibyte content (CJK/emoji at
3-4 bytes each) undercounts real size by up to ~4x, so a single-file diff
of several MB in bytes but under 1M code units slips past the cap and is
parsed and inserted into the result map — defeating the bound that keeps
huge single-file diffs from bloating memory/context in the diff viewer.

Measure with Buffer.byteLength. Add a regression: a diff under the cap in
code units but over it in bytes is now skipped.
2026-07-11 23:23:51 +08:00
0xfandomandGitHub 0369c86e3b fix(diff): number diff-snippet hunks by their new-file position (#1917)
getSnippetForTwoFileDiff builds each hunk's snippet from the new-file lines
(deletions are filtered out) but seeded addLineNumbers with the hunk's
oldStart. For the first hunk oldStart === newStart so labels are correct,
but any hunk following one that inserted or removed lines is mislabeled by
the net line delta of the earlier hunks — the shown numbers no longer match
the file the model is looking at.

Seed from newStart so the new-file content carries new-file line numbers.
Add a regression with a two-hunk diff whose second hunk shifts by +3.
2026-07-11 23:22:52 +08:00
Xiang HanandGitHub 4faf666cba fix: keep footer mounted across slash suggestions (#1943) 2026-07-11 23:22:23 +08:00
JATMNandGitHub 4f971a1316 fix(permissions): resolve relative worktree edit paths (#1930) 2026-07-10 16:35:57 +08:00
JATMNandGitHub 5918e330b2 fix(model): default NVIDIA NIM main loop model (#1928)
* fix(model): default NVIDIA NIM main loop model

* fix(cost): avoid default pricing for unknown models

* fix(model): use NIM descriptor default

* test(model): pin NIM descriptor fallback
2026-07-10 16:35:04 +08:00
Xiang HanandGitHub b588c26db2 fix(effort): keep effort indicator visible in prompt footer (#1919)
* fix: keep effort indicator visible

* fix: preserve footer status for effort fallback

* fix: simplify effort footer selection logic

* fix: harden effort footer fallback

* test: cover brief effort footer suppression

* fix: ignore empty footer notifications

* test: cover live effort footer updates

* fix: handle empty JSX footer notifications
2026-07-10 16:33:11 +08:00
JATMNandGitHub 9e06951e6e refactor(messages): extract content helpers (4 of 8) (#1901)
* refactor(messages): extract content helpers

* test(messages): cover extracted content helpers

* test(messages): statically import content helper

* test(compact): preserve assistant text helper semantics
2026-07-10 16:28:40 +08:00
JATMNandGitHub 64d164d207 fix(tools): keep HY3 tool schemas inline (#1923)
* fix(tools): keep HY3 tool schemas inline

* fix(hy3): parse documented tool wrapper

* fix(hy3): retain split wrapper openers

* fix(hy3): recover zero-argument tool calls

* test(hy3): restore fetch after XML stream tests

* fix(hy3): scope compatibility to Tencent
2026-07-10 09:39:25 +08:00
NikandGitHub 2259c809f7 fix(shim): don't infer Z.AI tool_stream for non-catalog GLM gateways (#1908)
The name-based shim matcher inferred the full Z.AI GLM contract —
including enableToolStreaming — for any glm-<n> model without a catalog
entry. tool_stream is a Z.AI-proprietary streaming extension, so serving
GLM through an arbitrary OpenAI-compatible gateway (e.g. NVIDIA NIM,
integrate.api.nvidia.com) made every request fail immediately with
400 Unsupported parameter(s): tool_stream.

Only a catalog entry may opt into tool_stream (Z.AI-contract gateways set
it explicitly via transportOverrides.openaiShim). Inferred GLM routes keep
the reasoning-shaping fields, which any GLM endpoint benefits from, but no
longer send tool_stream; without it tool calls are simply not streamed.
2026-07-10 09:38:32 +08:00
JATMNandGitHub 6d6b8de2ea refactor(messages): extract API cleanup helpers (8 of 8) (#1906)
* refactor(messages): extract API cleanup helpers

* fix(messages): strip nullable tool callers

* fix(messages): resolve extract review feedback
2026-07-10 09:37:54 +08:00
JATMNandGitHub 218064e376 fix(clipboard): fall back to image retrieval after Windows probe failure (#1922)
* fix(clipboard): run Windows Forms access in STA

* fix(clipboard): try image retrieval after Windows probe failure
2026-07-10 08:58:36 +08:00
0xfandomandGitHub 7995b9f492 fix(powershell): make CMDLET_PATH_CONFIG prototype-safe (#1913)
* fix(powershell): make CMDLET_PATH_CONFIG prototype-safe

CMDLET_PATH_CONFIG is a plain object literal keyed by the lowercased cmdlet
name, unlike its sibling maps CMDLET_ALLOWLIST (readOnlyValidation.ts) and
COMMON_ALIASES (parser.ts) which are Object.create(null) specifically to defend
against prototype-chain pollution. extractPathsFromCommand does
`CMDLET_PATH_CONFIG[resolveToCanonical(cmd.name)]` guarded only by
`if (!config)`. A command named 'constructor' or '__proto__' (both survive the
lowercasing) makes the lookup return an inherited Object.prototype member —
truthy — so the guard is bypassed and `[...config.knownSwitches]` throws
(spread of undefined), crashing path-constraint validation instead of treating
the name as an unknown non-path cmdlet. Give the map a null prototype like its
siblings so inherited-key lookups return undefined.

* test(powershell): assert set-content control resolves to ask decision

The control case previously only asserted checkPathConstraints did not
throw, so it would still pass if CMDLET_PATH_CONFIG stopped resolving its
own entries and set-content fell through as an unknown cmdlet. Assert the
returned behavior is 'ask' so the test proves the null-prototype map still
holds and classifies its own keys. Also reword the collision comment to
present constructor/__proto__ as representative reachable names rather than
the only ones.
2026-07-10 08:53:25 +08:00
JATMNandGitHub 07dd90db7b refactor(messages): extract plan mode rendering (7 of 8) (#1905)
* refactor(messages): extract plan mode rendering

* test(messages): cover plan mode reminder branches
2026-07-10 08:50:38 +08:00
0xfandomandGitHub 87c457c2fa fix(diff): count dropped lines correctly in truncated diff snippet (#1916)
getSnippetForTwoFileDiff caps the two-file diff snippet at
DIFF_SNIPPET_MAX_BYTES and appends a "[N lines truncated]" notice. When
the cut snaps back to a line boundary (the normal case), kept ends right
before the boundary newline, so full[cutoff] is that '\n'. Counting
newlines from kept.length already includes the boundary newline plus every
later one — which is exactly the number of dropped lines — so the
unconditional +1 double-counts it and the notice reports one line too many
(two if the snippet ends with a trailing newline).

Split the boundary and mid-line cases: at a boundary the count needs no
+1; only the mid-line cut (no newline within the cap) keeps a partial tail
line that must be added. Add a regression asserting kept + reported ==
total lines.
2026-07-09 22:49:47 +08:00
JATMNandGitHub eb5d719780 refactor(messages): extract basic factories (5 of 8) (#1902)
* refactor(messages): extract basic factories

* test(messages): expand factory parity coverage

* fix(messages): own factory synthetic constants

* test(compact): restore real message helpers per test

* fix(messages): preserve assistant tool scan behavior
2026-07-09 22:49:10 +08:00