* test: run the unit suite without --isolate (TA-001 round 2)
* test: clean up leaks exposed by running without --isolate (TA-001 round 2)
* test: clean up remaining leaks in shared-process runs (TA-001 round 2)
* test: reset query engine trace state before each test (TA-001 round 2)
* test: isolate env, process and global state leaks across randomized seeds (TA-001 round 2 batch 2)
* test: keep batch 2 state isolation from leaking into later files (TA-001 round 2 batch 2)
Drop the standalone processExitCode helper self-test. Adding a discovered test file changed native shard assignments and seeded file order, exposing existing cross-file mock contamination. The existing finalizer and skills integration tests retain exit-code assertions and continue to exercise the helper.
* 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.