* test: consolidate vendor catalog tests into per-vendor contract tables
* test: assert full descriptor limits and capabilities for nearai, longcat and xai
* fix: synchronize agent completion with output eviction
Agent task completion could become externally observable before the
output eviction and flush sequence completed, resulting in callers
observing empty output files.
Root Cause
----------
completeAgentTask() returned immediately while evictTaskOutput()
continued asynchronously. Callers could observe task completion
before the eviction/flush chain finished.
Fix
---
- Make completeAgentTask() async and await eviction
- Await completeAsyncAgent() at both call sites
- Clarify ORC-1337 vs gh-20236 in comments
Testing
-------
- AgentTool: 59/59 passing
- LocalAgentTask: 6/6 passing
Refs: ORC-1337, gh-20236
Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com>
* fix: move output barrier before terminal state transition
Review follow-up for ORC-1337 addressing both P1 findings:
1. Ordering: updateTaskState('completed') ran before the eviction
await, so TaskOutput(block=true) readers released by the terminal
status could still read a flushing file. The durable-output barrier
now precedes the state transition.
2. Sidechain transcript: local-agent output is a symlink to the agent
transcript written by the session-storage queue
(recordSidechainTranscript) — no DiskTaskOutput entry exists, so
evictTaskOutput alone resolved without waiting. completeAgentTask
now awaits flushSessionStorage() (drains the queue and in-flight
tracked chains) before evictTaskOutput and the status flip.
Also per review:
- Remove ORC-1337-FIX.md from the PR (P3)
- Add deterministic regression test gating both barriers
(completionBarrier.test.ts) — verified to fail on the previous
ordering
- Update call-site comments to describe the corrected sequence
Testing: AgentTool + LocalAgentTask 67/67 (incl. 2 new); tsc clean.
Refs: ORC-1337, gh-20236
Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com>
* fix(agent): await scoped output writes before completion
Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com>
* fix: cancel agent transcript waits without delaying cleanup
Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com>
* fix: preserve agent output ownership across session changes
Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com>
* fix: await agent output link replacement after clear
Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com>
* fix: preserve agent output destination and finishing input admission
Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com>
* fix(agent): preserve output destinations for background and resumed sessions
Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com>
* fix: preserve agent output ownership across replacement and clear
Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com>
* fix(agent): bind retained output links to the recording producer
Record successful sidechain append destinations by agent ID and require
retained output links to match that producer record. Preserve the record
across session clears and project overrides for retained resumes.
Cover swapped agent and project targets for captured and live writes.
Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com>
---------
Co-authored-by: jatmn <jatmn@users.noreply.github.com>
Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com>
* ci: run unit test shards in parallel
* fix(ci): address shard-selection review feedback
* ci: cache package downloads in the shard and dedicated test jobs
* ci: cache package downloads in PR checks
* ci: cache package downloads in the suite job
* ci: fix suite cache key and separate root and web cache prefixes
Resolves CodeQL js/insufficient-password-hash (alert 70):
aimlapiByKeyIdentity fed the API key into a plain SHA-256.
AI/ML API by-key top-ups (src/integrations/aimlapi, ProviderManager)
- The identity is now `key:v2:` + HMAC-SHA256 keyed BY the credential over
a fixed label, so the key is never the hashed input. It stays cheap on
the interactive path (no KDF stalling Ink before the async lock), and a
known-answer test pins the value so the derivation cannot drift silently.
- Receipts written by earlier versions carry `key:<sha256>`. That value is
deliberately never recomputed, so they are handled without hashing:
- one that stores this credential (a settled saved-key top-up waiting on
its profile write, which a claim otherwise refuses to replace even with
abandonExisting) is matched by its stored key and upgraded under the
lock, in both reconcileSettledAimlapiTopupStateAsync and the by-key
claim (new `byKeyApiKey` claim option, passed by ProviderManager);
- one with no stored key (an in-flight by-key checkout, or a settled
env-key top-up) cannot be attributed and holds nothing unrecoverable,
since a by-key payment is credited to its key server-side, so the next
claim may replace it instead of refusing a re-run that can never match.
Both are skipped while a mint/exchange lease on the record is live.
Skills watcher test (src/utils/skills)
- `disabled sibling edits stop triggering hooks…` flaked on loaded CI
runners ("Expected number of calls: 1, Received: 2"). Its edit helper
retries writes every 50ms until a hook fires, and a retry already in
flight could land one more hook after the exact-count assertion or after
the sibling watcher closed. Assert that each phase's edit was observed,
wait until hooks are quiet between phases, and give the real-filesystem
test a 15s budget. Under 12 saturated cores: original 2/15 failures,
fixed 45/45 passes.
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
CodeQL default setup flagged js/insecure-randomness (alert #72, high)
on the custom-pricing budget fixture, which built message uuids from
Math.random(). The values never leave the test process, but the field
is typed as UUID and the same pattern appeared in three places across
two fixtures.
Generate the uuids with crypto.randomUUID() instead. This clears the
alert, matches the Message uuid type, and drops the
`as Message['uuid']` cast in the manual-compact cooldown fixture.
Nothing depended on the old user-/assistant-/test- prefixes.
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
* feat(skills): load skills from ~/.agents/skills and opt-in ~/.claude/skills (#2258)
Read user skills that other coding agents keep in their own homes,
read-only and after every OpenClaude-owned skill source:
- ~/.agents/skills (shared cross-agent home used by Codex and
`npx skills`) loads by default; agentsSkillsEnabled turns it off.
- ~/.claude/skills (Claude Code's private config dir) is opt-in via
claudeCodeSkillsEnabled, keeping the #1875 isolation default.
Both settings are honored from user, local, flag and policy settings
only; project settings cannot opt a user in. Sibling skills never shadow
anything: same-file dedup keeps the OpenClaude copy of a symlinked skill,
and a sibling skill is dropped when any skill-dir, dynamic, plugin or
bundled skill or built-in command owns its name. When the skill listing
is over budget, sibling skills go names-only before any OpenClaude skill
description is trimmed.
/skills and `openclaude skills list` group and label sibling skills and
hint at disabled homes that hold skills; /config gets a toggle for each
home; `skills remove` refuses them; enabled homes are watched. The
skills CLI now registers bundled skills and built-in plugins like a
session does, so name collisions resolve the same way.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(skills): respect pinned sibling settings in /config and start the watcher on toggle
- /config hid a sibling skills toggle only when managed policy set it.
Local and --settings values outrank the user settings the toggle
writes, so flipping it there changed nothing. resolveSiblingSkillsHomes
now reports pinnedBy for any source that outranks user settings;
/config hides that toggle and the /skills hint skips that home.
- Turning a home on in /config added it only to a running watcher. With
no skill directory at startup there was none, so edits in the newly
enabled home never hot-reloaded. The reload now starts the watcher.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(skills): reconcile sibling source state and CLI bootstrap
* fix(skills): preserve activation during command refresh
* test(skills): wait for observed watcher edits
* fix(skills): reconcile sibling sources and fence stale loads
* fix(skills): reserve owned invocation names against sibling display names
* test(skills): allow bounded subprocess regression to finish
* fix(skills): preserve sibling identity priority and watch linked skills
---------
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
Co-authored-by: jatmn <the@jat.mn>
* fix(model): enable model discovery for remote custom OpenAI-compatible providers
Only skip descriptor discovery for local custom providers (Ollama, LM Studio)
that rely on the legacy scoped-cache path. Remote custom providers (custom
gateways, self-hosted APIs) now proceed with descriptor discovery so their
dynamically-discovered models appear in the /model picker and /model refresh
works.
Previously, the routeId === 'custom' guard unconditionally returned null,
preventing ALL custom providers from using descriptor discovery. Remote
providers fell back to hardcoded default options while local providers
correctly used the legacy path via getAdditionalModelOptionsCacheScope().
* fix: fall back to global disabledMcpServers when project has none configured
When a project directory has no disabledMcpServers entry in its per-project
config isMcpServerDisabled() returns false for every server, causing all
configured MCP servers to be enabled in that project.
- isMcpServerDisabled() now falls back to the global disabledMcpServers list
when the project config does not explicitly set one (undefined).
- setMcpServerEnabled() persists toggles both to the project config and
the global config, so the enabled/disabled state applies uniformly
across all projects.
- GlobalConfig type gains the optional disabledMcpServers field.
* fix: skip global disabledMcpServers write for builtin default-disabled servers
Builtin servers track state through enabledMcpServers (allow-list), and
isMcpServerDisabled() returns early on that path without reading
disabledMcpServers. Guard the global write with isDefaultDisabledBuiltin()
to avoid polluting the persisted config with builtin server names.
* fix: address review feedback on #1935
- M1: Only propagate disable to global (enable stays project-local)
- L1: Add disabledMcpServers to GLOBAL_CONFIG_KEYS
- L2: Add comprehensive test coverage for MCP config toggles
- Update PR description to justify model.tsx bundling
M2 (empty-list key removal) intentionally omitted — conflicts with M1:
enabling a server that exists in global list would remain disabled.
* fix: make config tests resilient by directly manipulating test config objects
Avoid relying on saveCurrentProjectConfig for test setup since its
test-mode Object.assign implementation cannot delete keys.
* debug: add NODE_ENV logging to config test
* fix: use saveCurrentProjectConfig instead of direct property set
Direct property set on getCurrentProjectConfig() returns different
objects between calls in CI, but saveCurrentProjectConfig correctly
updates the shared test singleton.
* fix(mcp): handle global fallback enable/disable edge cases
- When enabling a server disabled only via the global fallback, explicitly
materialize an empty project-level disabledMcpServers to override it
- When disabling a server on a project with no override, initialize the
project list from the inherited global list to preserve existing disables
- Add sharedMutationLock to config.test.ts for suite safety
- Reset disabledMcpServers to undefined in afterEach cleanup
- Add regression tests for global fallback enable/disable interactions
* fix(mcp): preserve inherited disables and narrow PR scope
* Preserve concurrent MCP disables in first project override
* Publish stale inherited MCP toggles even when disk matches
* fix(mcp): publish local toggles after peer project overrides
---------
Co-authored-by: jatmn <the@jat.mn>
* fix(model): enable model discovery for remote custom OpenAI-compatible providers
Only skip descriptor discovery for local custom providers (Ollama, LM Studio)
that rely on the legacy scoped-cache path. Remote custom providers (custom
gateways, self-hosted APIs) now proceed with descriptor discovery so their
dynamically-discovered models appear in the /model picker and /model refresh
works.
Previously, the routeId === 'custom' guard unconditionally returned null,
preventing ALL custom providers from using descriptor discovery. Remote
providers fell back to hardcoded default options while local providers
correctly used the legacy path via getAdditionalModelOptionsCacheScope().
* fix: restore routeId === 'custom' fallback for legacy OpenAI discovery
* test(model): add remote custom route regression test
* fix(model): forward custom auth headers to descriptor discovery for remote custom routes
- Add OPENAI_AUTH_HEADER forwarding in getOpenAIDiscoveryRequestOptions
- Update remote-custom test to verify descriptor discovery path
- Fix test for pooled credential to expect {} headers instead of undefined
* fix(model): address review feedback - auth headers, cache keys, fallback
- P1: Properly replicate inference auth header logic (OPENAI_AUTH_HEADER +
OPENAI_AUTH_SCHEME + apiKey fallback matching the shim)
- P1: Update findCachedCatalogEntryForApiName and
getNvidiaNimDiscoveryCacheKeyForEnv to include OPENAI_AUTH_HEADER in cache
key calculation so readers match the picker's partition
- P2: Fall through to legacy OpenAI path when dynamic discovery catalog has
no usable entries (no static models and no cached discovery)
* fix(model): keep custom discovery repair scoped and auth-consistent
* fix(discovery): preserve custom credentials across refresh
* fix(model): retain filtered custom fallback and isolate cache mocks
---------
Co-authored-by: jatmn <the@jat.mn>
* fix: complete full checks and repair baseline test failures
* fix: bound CI tests and avoid synchronous compiler hangs
* fix: avoid isolated Bun stdio pipe failures in CI
* test: address CI repair review findings
* docs: cover fixture helpers counted by review
* feat(sessions): add /sessions to resume or delete saved conversations
- New /sessions command: lists this repo's saved conversations; Enter opens
Resume / Delete / Back; delete asks for confirmation (Cancel by default);
`d` pressed twice deletes the focused session.
- Deleting removes the transcript and its sidecars (.cast, .replay.json,
<id>/), refuses the active session and validates the id and path.
- Fix: late metadata writes no longer recreate a deleted transcript.
- Fix: launches that only run slash commands no longer create a session or
print a resume hint at exit.
Closes#2223
* fix(sessions): keep writing to a transcript whose delete failed
- deleteSessionFiles: if removing the transcript fails, unmark it so
later writes are not dropped while the file is still on disk
- forgetDeletedSession: also wait for in-flight direct appends to the
deleted path, not only queued writes
- docstrings for sessionKey, isDeletedSessionFile, restoreSessionWrites
* fix(sessions): wait for every removal before restoring writes
A sidecar failing fast made Promise.all reject while the transcript was
still being removed, so writes were restored and could recreate it.
Settle all removals first; restore only if the transcript survived, and
treat a leftover sidecar as an orphan once the transcript is gone.
* fix(sessions): restore writes when the deletion barrier fails
forgetDeletedSession marks the transcript before flushing; if a pending
write fails, flush rejects and nothing is removed, but the transcript
stayed marked and later writes were skipped. Undo the mark and rethrow.
* fix(sessions): keep the list usable when a resume fails to load
loadFullLog ran outside any error handling, so a transcript that failed
to load left the dialog busy and ignoring input. Report the error, clear
busy and return to the list.
* fix(sessions): do not resume a transcript that failed to load
loadFullLog falls back to the message-less lite log when a transcript
can't be read, so resuming it opened an empty conversation. Treat that
fallback as a load failure, and test it with the real loader.
* fix(sessions): drop buffered entries when the session changes
Local-command entries are buffered until the first real message. If the
active session switched before that, they were written into the new
session's transcript. Track the session that owns the buffer and drop it
on a switch.
* feat(provider): add GMI Cloud as a provider and official partner
Add GMI Cloud (https://www.gmicloud.ai/) as a first-class
OpenAI-compatible provider and list it in the partner roster.
Provider (`gmi-cloud`):
- Gateway descriptor for https://api.gmi-serving.com/v1 with a hybrid
catalog: a short curated list plus authenticated /v1/models discovery.
Defaults to deepseek-ai/DeepSeek-V4-Pro.
- GMI_API_KEY is a dedicated credential. A generic OPENAI_API_KEY is
never sent to GMI, and the GMI key is never mirrored to another host.
- Reachable from /provider, `--provider gmi-cloud` (which also reads an
optional GMI_MODEL) and saved profiles, including relaunch.
- `--provider gmi-cloud` drops an API format or custom auth header left
by another provider, since GMI is Bearer-only chat completions.
- Routes by its default base URL only. The same host also serves GMI's
Anthropic-compatible API, which stays a custom Anthropic endpoint.
- Forwards GMI_API_KEY to spawned teammates.
Partner:
- README partners table and the website roster, with light and dark
logos under docs/assets/ and web/public/partners/.
Docs: README provider row, .env.example and the website provider and
configuration pages. Also corrects the README line that said the
Opengateway routes GMI Cloud models, which it no longer does.
* docs(readme): use Twigpine's dark logo in dark mode
The partners table showed the light Twigpine logo as a white tile on GitHub's dark theme. Serve https://twigpine.com/logo-dark.png there, the same markup the Zero README uses.
* fix(gmi-cloud): curate only models GMI's /v1/models serves
A live /v1/models call (79 models) shows GMI does not serve
anthropic/claude-sonnet-4.6 or anthropic/claude-opus-4.7 on the
OpenAI-compatible API, so drop them from the curated seed. DeepSeek V4
Pro and V4 Flash (GMI's own coding picks) remain; discovery lists the
rest.
Tests now use the item shape the live endpoint returns (`name`,
`context_length`, `pricing`, no output limit), and the GMI_MODEL
examples use a served model id.
* feat(gmi-cloud): default to Qwen 3.8 Max and curate Qwen 3.8 Max and Flash
GMI Cloud's partner models are Qwen 3.8 Max and Qwen 3.8 Flash. Add
shared descriptors for both (context as GMI's /v1/models reports it,
262,144 and 1,048,575; a conservative 65,536 output cap since GMI does
not publish one), curate them ahead of the DeepSeek V4 picks, and make
Qwen/Qwen3.8-Max the route default.
Verified live through a built CLI: read-a-file then write-a-file agent
turns succeed on both models, and `--provider gmi-cloud` with no
--model uses Qwen/Qwen3.8-Max.
* fix(gmi-cloud): keep the GMI key on GMI's HTTPS /v1 API (review feedback)
Address CodeRabbit's three findings on #2279:
- isGmiCloudBaseUrl now accepts only HTTPS on the default port, host
api.gmi-serving.com, and a /v1 path or one below it. Plain HTTP,
custom ports, the host root (GMI's Anthropic-compatible API) and
other paths are rejected.
- buildGmiCloudProfileEnv no longer falls back to an ambient
OPENAI_BASE_URL, which may belong to another provider. It keeps an
explicit GMI URL and otherwise uses the route default, so the key it
returns always targets GMI.
- The profile env mirror sets GMI_API_KEY only when the profile's URL is
GMI's, not for any gmi-cloud profile, matching the startup checks.
Each fix has a test that failed before the change.
* fix(gmi-cloud): withhold the GMI key from a retargeted gmi-cloud profile
Address CodeRabbit's follow-up on #2279. Applying a gmi-cloud profile
whose base URL is not GMI's inference API installed the saved key as
OPENAI_API_KEY for that URL. Withhold it there, the same way this
function already treats retargeted ApiSmart, Concentrate, API Route,
LLMTR and Command Code profiles.
A gmi-cloud profile with no base URL now resolves to the GMI default
(like those routes), and the GMI_API_KEY mirror follows the resolved
URL.
Tests cover the retargeted profile in-session (neither key installed),
the empty-URL profile, and the persisted startup env of a retargeted
profile (always GMI's URL, never the other host). The first two failed
before this change.
The site's canonical address, sitemap and docs links now use
twigpine.com hosts (openclaude, api, ads, explorer). The old
openclaude.gitlawb.com address forwards every path to the new one.
The repo mirror link pointed at a path that returned 404; it now links
the Explorer page that resolves.
The npm rename has shipped: @twigpine/openclaude@0.32.0 and the one final
@gitlawb/openclaude@0.32.0 are both published, and the old package is
deprecated. The pieces that existed only to get that final old-name release
out can go.
Release workflow
- Remove the legacy-scope-gate job, the legacy steps in install-verify and
the publish-npm-legacy-scope job. The job graph is back to
release-please, install-verify, publish-npm and docker, so a release no
longer depends on a registry lookup for the old package.
- Keep what proved itself in the 0.32.0 release: skipping a version that is
already published, publishing the verified dist/ with --ignore-scripts,
and the provenance and trusted-publisher line in the run summary.
App code
- Remove src/utils/packageRename.ts, the package-renamed startup notice, and
the early exits in `openclaude update` and `/update`. They only fire in a
build published under the old name, and no such build will be made again.
- Remove the tests that covered them, including the release guard test that
held the legacy wiring together.
Docs and website
- X links point at x.com/twigpine (README badge and community link, the
website community list and llms.txt).
- The website install page said Node.js 20 or newer; the package requires 22.
- The upgrade notes for people coming from @gitlawb/openclaude stay.
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
The npm package moves from the @gitlawb scope to @twigpine. Everything that
names the package now uses the new name, and the release workflow can create
the new package and close out the old one.
Rename
- package.json, bun.lock, release-please config, docs, website, VS Code
extension and tests use @twigpine/openclaude.
- scripts/build.ts takes the baked-in package name from package.json instead
of repeating it; scripts/verify-clean-install.ts does the same.
One final release under the old name, then nothing
- Shipped builds have no working auto-updater (the no-telemetry plugin stubs
it out), so people update by re-running the install command. Anyone still
typing the old name lands on whatever is latest there.
- release.yml publishes this release once more as @gitlawb/openclaude. That
build shows a startup notice, and `openclaude update` and `/update` print
the same note and stop, saying the package moved and how to switch.
- legacy-scope-gate limits this to a single release. It only runs while the
old name's latest version is still 0.31.0.
- There is no in-place migration. Both packages own the `openclaude` bin, so
npm refuses to install one over the other, and a forced install is undone
by uninstalling the old package. The docs give the safe order.
Tokenless releases (OIDC only)
- Both publish steps authenticate through npm trusted publishing and attach
provenance; no npm token is used anywhere. npm only attaches a trusted
publisher to a package that already exists, so @twigpine/openclaude is
created once by hand as an empty 0.0.0 placeholder before its first release.
- Publish steps skip a version that is already on the registry, publish the
verified dist/ with --ignore-scripts, and report provenance and whether a
trusted publisher was used in the run summary
(scripts/npm-publish-report.ts).
- The legacy-named build is built and install-verified before anything is
published.
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
* chore(rebrand): update Gitlawb references to Twigpine (#2246)
The GitHub org has been renamed to Twigpine, so point repo URLs, badges,
CI repo-identity guards, and user-facing sponsor/partner labels at the new
name.
Scope is deliberately limited to references that are safe to change today:
- GitHub URLs (github.com, api.github.com, raw.githubusercontent.com),
including the skills registry and release-please release notes
- The release-please release trigger, which gates on the repo slug and
would otherwise stop publishing
- User-visible display strings: gateway label/name, AI/ML API partner
name, sponsored-tip sponsor, and docs/site prose
Left on the old name because the rename has not landed for them:
- `@gitlawb/openclaude` npm scope (the package does not exist on npm)
- opengateway/ads/docs subdomains, which resolve only under gitlawb.com
- The `gitlawb-opengateway` preset id, tip ids, and gateway file names,
which are persisted in user config and sent on the wire
* fix(aimlapi): stop matching topup intent on partnerName (#2246)
partnerName is display metadata that travels alongside the partner id, not
part of the intent identity. It is now excluded from INTENT_KEYS, so
matchesIntent no longer requires it to be unchanged.
Without this, renaming the partner on the Gitlawb -> Twigpine rebrand would
have stranded any in-flight topup: a receipt persisted under the old name
fails the strict equality check, so loadAimlapiTopupState returns null and
the user restarts checkout. partnerId remains the identity field, so
receipts are still scoped to the correct partner.
* fix(web): use current Twigpine artwork for the partner card (#2246)
The partner asset was a byte-identical rename, so the roster row rendered
the former GitLawb key/branch symbol under the Twigpine name while the
README pointed at the official tree mark. Vendors the current artwork from
twigpine.com/logo.png, resized to the roster's 256x256 convention, keeping
the self-hosting contract in web/src/data/partners.ts.
---------
Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
The Read dedup stub only fired for the exact same range with an unchanged
mtime. Two common cases re-sent content the model already had:
- mtime moved but bytes did not (git checkout/stash pop, formatters, touch)
- a sub-range of a file that was already read in full
Extract the eligibility check into readDedup.ts. On an mtime mismatch, re-read
the stored range and compare it byte-for-byte before deduping, refreshing the
cached timestamp on a match. Treat an in-bounds range of a prior full Read as
already seen. Edit/Write entries, partial views, out-of-range offsets, and any
stat/read failure still fall through to a normal read.
getWindowsBashHookCommand prepends `bash` so a directly invoked script
executes instead of opening in the default file handler. The suffix test
was case-sensitive, but the behaviour it works around is not: Windows
picks a handler by extension without regard to case, so `./hook.SH`
opened in an editor exactly as `./hook.sh` would.
A hook that never ran is the quiet kind of failure - on UserPromptSubmit
it blocks the prompt, and the only trace is a DEBUG_SDK session log.
Three cases added to the prefix list (`./hook.SH`, `./hook.Sh argument`,
a quoted uppercase path) and two to the preserve list, so an uppercase
suffix that is not the command word still changes nothing. Without the
flag those three are the only failures in the file.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Some OpenAI-compatible providers end a stream twice: one chunk carrying
the finish_reason, then a second, identical but for the usage, with
choices still populated. The final-usage fallback required an EMPTY
choices array, so that second chunk was skipped and its usage dropped.
The streaming loop ignores a repeated finish_reason by design, so nothing
else picked it up: every assistant message from such a provider stored
usage of all zeros. Anything reading the size or the cost of a
conversation reads it from there - tokenCountWithEstimation walks back to
the newest message carrying usage and trusts it - so automatic compaction
on size could not fire, the context warning never warned, and cost
tracking recorded nothing. A measured session reached 1763 messages with
preTokens: 183 recorded at its compaction boundary.
Dropping the empty-choices requirement leaves hasEmittedFinalUsage as the
single guard against a double emit, which the existing empty-choices and
both-terminal-chunks tests already cover.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Maintainer confirmed sharp stays intentionally opt-in on published
installs. Document that clipboard paste / local FileRead decode need
`npm i -g sharp`, while pre-encoded URL/base64 passthrough does not.
* feat(search): make Exa the default web search backend and add /search
Exa now leads the auto chain: keyed Exa (EXA_API_KEY) runs first, other
keyed backends next, then a new keyless Exa free-tier adapter (Exa's hosted
MCP endpoint), with DuckDuckGo as the last-resort fallback. Built-in search
for Anthropic/Vertex/Foundry/Codex is unchanged. WEB_SEARCH_PROVIDER=exa
uses the keyed API when a key is set, otherwise the free tier.
New Exa options: EXA_SEARCH_TYPE, EXA_NUM_RESULTS, EXA_FREE_TIER (opt-out).
The free-tier adapter handles all three rate-limit reply shapes, spaces
calls to Exa's default QPS, backs off for five minutes after a limit, and
parses Exa's plain-text result blocks defensively.
New /search command: pick a backend, add or remove an API key via a masked
input, show status, run a test search. Choices are saved to the env block
of ~/.openclaude.json (0600) and applied immediately; settings.json env
entries that override them are reported instead of silently losing.
Security: isSensitive arg redaction now also covers local-jsx commands
(/ads, /search), which previously recorded raw inline args in the
transcript and model context.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(search): fall through when domain filters empty the Exa free-tier hits
The keyless Exa endpoint has no server-side domain filters, so when the
client-side filter removed every hit, runSearch returned a successful empty
result and never tried DuckDuckGo, which can honor the filter. Providers can
now set `fallbackInAuto` on their output; auto mode skips to the next
backend on that signal, and returns the filtered-empty result only if no
later backend does better. Genuine no-results replies and explicit `exa`
mode are unchanged.
Also adds doc comments to the touched functions that lacked them.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
---------
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
* fix(prompt): prevent color-only banner layout regression
* test: fix standalone banner regression cleanup
* fix: preserve team border identity and validate color-only prompts
* fix: suppress standalone banners while team identity is active
* fix: resolve prompt color from AppState team members
Resolve explicit self color, matching self/leader member color, then dynamic teammate color. Preserve mode priority and suppress saved standalone colors for active teams.
Validation (Windows, Node 24.21.0, Bun 1.4.2):
- PromptInput tests: 21 passed, 0 failed, 42 assertions.
- Frozen install, typecheck, type tests (10 files), build/smoke/deadcode, both CLI version checks, diff check and PR intent scan passed.
- Full check incomplete: hung at repo-map timeout cancellation; isolated PR test passed, isolated unchanged base 9a2910da also hung. Both stuck test processes were terminated. Seven launcher failures preceding the hang match recorded base failures.
- Provider: 1701 passed, 1 failed; watchdog failure reproduced on base in prior comparison.
- Recommendation: 164 passed, 1 failed; default-profile fallback failure reproduced on base.
No provider, launcher, or repo-map code changed. Clean-runner CI remains required.