Repository navigation
Releases: risunCodex/Cartethyia
Release list
Cartethyia 2.7.3
Cartethyia 2.7.3
[2.7.3] - 2026-10-30
Instalasi ulang.
2.7.3di-registri npm telah diperbarui sekali setelah rilis
pertama (tarball baru memuat perbaikan qimei36 dan glass-blur). Jika instalasi
yang sudah ada bermasalah setelahnpm update— terutama error integritas
(shasum/integritymismatch) saatnpm install— uninstall dulu lalu
install ulang:npm uninstall -g cartethyia npm install -g cartethyiaData aman: konfigurasi, database, dan state tersimpan di direktori data
gateway (~/.config/cartethyia/~/.cartethyia), bukan di dalam paket,
sehingga uninstall/reinstall tidak menghapus apa pun.
Added
- Inject System Prompt — tenant-scoped, three precedence levels. A new
Overview panel lets a tenant append (or override) a custom system prompt
globally, per provider, or per exact model, resolved most-specific-wins with
a follow-global/custom toggle on every provider. Append is idempotent and
lands after all existing system content; override replaces only the caller's
system content, never the provider's own mandatory identity prompt. Providers
whose upstream validates their fixed system prompt as a channel identity
(CodeBuddy, CodeBuddy CN, WorkBuddy) receive the tenant text wrapped in
<cartethyia:system-prompt>markers after their identity prompt — appended
in the same turn, which the upstream accepts. Settings persist in a dedicated
tenant_system_promptstable (migration0031_tenant_system_promptsfor
both Postgres and SQLite; 5 MB text cap per scope), read through the
revision-keyed cache so edits are live on the next request, with console
routes (GET/PUT/DELETE /system-prompt) and per-scope append/override,
.txt/.jsonimport, and search in the dashboard. Design record:
documentation/proposals/inject-system-prompt.md. - Invoke kill switch moved into API Credentials. The tenant-wide
"deny all keys" breaker is now a Disable/Enable Invoke button in the API
Credentials header instead of a standalone Overview card, confirm-gated:
disabling refuses every/v1request with 503 while the dashboard stays
reachable to re-enable. - Content-derived dashboard build ID. Every production build fingerprints
its emitted assets and embeds the ID as acartethyia-dashboard-buildmeta
tag; the server echoes it asX-Cartethyia-Dashboard-Buildon HTML
responses. Comparing that ID (plus the content-hashed CSS URL) tells you
exactly which build a deployment serves. - GitHub Copilot Auto routes from the selection itself. The
/auto
selection response names the model and its supported endpoints, so the
adapter picks the Responses-vs-Chat wire from the selection and drops the
follow-up/modelsround trip on the hot path (older responders without
endpoints still fall back to the roster lookup).
Changed
- Adaptive Postgres pool. The pool defaults to 8 connections with a
30-second shrinker that trims idle connections down to a configurable floor
(DATABASE_POOL_IDLE_FLOOR, default 2), so a burst of dashboard or proxy
load no longer leaves a hundred idle sockets pinned forever. - Routing: one CallerIdentity, one RoutingDecision per candidate. The
layered re-filtering in route planning (usage floor → slow lane → health →
plan gate → capability, each with its own re-sort) is merged into a single
per-candidate decision and one rank sort (fresh → slow → cooling →
saturated). Provider rotation now only reorders within the same rank, so a
cooling account can never float back to the front. Candidates carry the
account's owning tenant, and admission buckets scope shared accounts
globally while tenant-owned accounts are scoped per owner — two tenants can
no longer drain each other's concurrency ceiling on an account only one of
them owns. - Overview polish. The Compression panel's transforms render as contained
sub-cards with active/off badges and use the shared banner primitive for the
quality warning; the gateway-endpoint, remote-support, and status blurbs
read like an operator wrote them.
Fixed
-
Cursor continuations survive a model switch. A continuation pinned to an
account that no longer serves the (translated) model used to fail with a
strict 400. Pin-empty continuations that carry complete history are now
rebaseable: they fall through to normal routing and start a fresh Run, while
live pending calls stay hard-pinned so nothing leaks across accounts. -
A changed model cooldown invalidates the route snapshot. Cooldown
overrides took effect on the next snapshot rebuild rather than the next
request; a cooldown mutation now invalidates the snapshot immediately. -
Share links can mint at most one child key per canonical client IP.
Enrollment links are public and IP rotation previously let one attacker mint
unbounded children; dedupe now keys on the canonical client address. -
Share usage accounting survives key deletion. Revoked or deleted child
keys kept their spend counted in family totals (template counter + telemetry
sums), quota windows render distinct resets, and a template can cap its
active children (max_children, migration0030) with recipients listed
server-side paginated/sortable and a batched bulk-revoke endpoint. -
Share template policy is no longer duplicated into children. Children
inherit policy at resolution time instead of carrying a copied row
(migration0029nulls the duplicated columns; restore normalizes), and the
API key list orders parents before their share children. -
The chat wire emits only its selected token-limit field. A request
carryingmax_completion_tokensechoed that alias alongside the bounded
max_tokens, and a strict upstream refused the double field; the alias is
now deleted in either direction. -
DeepSeek-style reasoning replay stays present on resold models. Models
served through a DeepSeek-family upstream kept their reasoning replay
marker even when the canonical surface stripped it, avoiding upstream
rejections on follow-up turns. -
The OAuth browser dialog no longer completes on a stale account
snapshot. (pre-2.7.2 carry-over, recorded here for completeness.) -
Buddy device binding survives account export/import. The buddy family
(CodeBuddy, CodeBuddy CN, WorkBuddy, WorkBuddy CN) binds a per-account
device identity (qimei36, sent asX-Device-Id-List) that defaults to a
derivation from the internal account id. Export/import mints a new account
id, so a re-imported account silently changed device identity and the
upstream channel check (11128) rejected it. The export now materializes the
resolved binding into the exported auth state whenever the account does not
already pin one, so a re-import presents the exact same device. -
The dashboard's glass blur now survives production builds. The standard
backdrop-filteris declared after its-webkit-counterpart in every
glass surface, so a compliant browser applies the standard declaration last
instead of the prefixed one; a cascade test locks the ordering on both
stylesheets. (This is the build-blur fix, formerly diagnosed as a cache
issue.) -
Qoder native machine identity, bound per account. The daily 100-credit
campaign grant is only revealed to a native device identity (UMID), so
pooled accounts now own one:provider_accounts.native_identity(migrations
0026_add_qoder_native_identityfor both Postgres and SQLite, DB-enforced
one-slot-per-env uniqueness), a Drizzle-backed identity store with
in-process allocation (qoder-native-identity-store.ts), and check-in
behavior that probes the account's bound identity first before enumerating,
persisting the identity that actually wins the claim.0027marks the
genuine-seed carrier (locale_cfg) as persisted alongside the identity. -
Qoder identity mint pipeline.
qoder-umid.tsmints genuine device
identities by replaying the vendor bridge (vendor/qoder-umid/umidpack/—
Linux ELF ×86-64/aarch64, macOS Mach-O ×86-64/arm64, Windows PE + sgsdk.dll,
plus the 3.7 MB mini-bundle rootfs), with flock-serialized seed staging on
WSL and a documented generated-identity fallback.scripts/tools/extract-qoder-umid.ts
re-extracts the bundle from an official client. -
Native identity management in the dashboard. A per-account
Native Identity dialog (qoder/qodercn account rows): bound status and slot,
copy/paste JSON export-import for moving an identity between installs, and
regenerate. Console routes carry per-account auth (platform admin for shared
accounts, tenant scope otherwise), and backup export/restore round-trips
native_identityverbatim. -
Qoder research record. The reverse-engineering evidence now lives in-tree:
the daily-credit campaign recipe (research/qoder-daily-credit-campaign.md),
the UMID bridge internals (research/qoder-umid-reverse.md), the identity
gate post-mortem, the Pro-trial activity-claim endpoints found by unpacking
the shipped client (research/qoder-trial-activity-endpoints.md— including
the content-length-string signature scheme, the claim-API family, and the
provision matrix that settles provisioning as server-side cohorting), and the
relocated Buddy device-binding research (research/buddy-device-binding.md).
Skills updated so future sessions inherit the proven method: provider catalog
gainsreferences/qoder-identity-and-claims.md(mint/replay/exchange rules,
claim-safety order, the DEADpro-upgradesurface), and reverse-engineering
notes the two techniques that cracked it (literal-window grepping; capturing
the client's own signed request by hooking its shared-process bundle, restored
byte-for-byte afterward).
Changed
- Antigravity's static catalog matches the roster. The offline model list
is pruned to the ids the account actually serves — th...
Cartethyia 2.7.2
Cartethyia 2.7.2
[2.7.2] - 2026-10-29
Added
- Codex CLI remote model mapping. Codex now participates in the CLI Tools
remote-routing table like Claude Code: the Codex page gains the "CLI asks
for → Cartethyia routes to" mapping surface, and the inbound User-Agent
(codex-tui/…,codex-cli/…) is recognised forrouting:cli_mapping.
The Codexreviewslot is first-class with its real source model
(codex-auto-review), so the Guardian approval-review model can be routed to
a serving provider instead of answeringaccounts_unavailable. - Ollama Cloud provider catalog. A static floor for the six public models
(gemma4:31b, gpt-oss:20b/120b, nemotron-3-nano:30b/-super/-ultra) with
live limits from the public roster, so the provider works before discovery
runs.
Changed
- BYOK providers can set a route client identity. The Routing Strategy
"Client identity" control now applies to custom (BYOK) providers too: Codex
/ Claude Code / None. The operator's explicit choice wins over the adapter's
default, andNoneis honored as no identity at all.
Fixed
- Qoder "insufficient tool messages" after cancelled/compacted parallel
calls. An assistanttool_callsturn whose sibling call had no result
used to be withheld while the survivingtoolresult turn was still sent,
producing an orphantoolturn the upstream rejected
("Messages with role 'tool' must be a response to a preceding message with
'tool_calls'"). The assistanttool_callsemit and thetoolresult turns
now share one source of truth, so a transcript is never half-stripped. - Responses parallel
function_callitems fold into one assistant turn.
Clients that send severalfunction_callinput items back-to-back (opencode,
Codex) decoded into separate canonical assistant turns, so the Qoder/OpenAI
wire saw an assistanttool_callsturn before the previous one's results —
rejected as "An assistant message with 'tool_calls' must be followed by tool
messages responding to each 'tool_call_id'". Consecutive tool-call turns now
merge (a leadingreasoningitem included). - Responses
additional_toolsitem no longer rejects Codex auto-review.
Codex's Guardian approval reviewer declares its review tools with an
additional_toolsinput item; parsing it threw
("Unsupported Responses input item type"), which failed the whole
auto-review request and made every permission escalation look like an
infrastructure error. The item is client-side metadata and is now skipped
safely. - Responses "Encrypted reasoning" robust to non-string payloads. A
reasoning item whoseencrypted_contentis not a string no longer throws;
it falls back to the readable reasoning, and the encoder only emits
encrypted_contentwhen it is a string. - Trae context-overflow streams classify as 413. The upstream error code
4026 ("context length exceeded") now maps tocontext_length_exceeded
(413) instead ofplatform_unavailable(502). - OAuth browser login no longer completes on a stale account snapshot. The
dialog used a cached account baseline from an earlier login, so a second
login could report success before anything happened. The baseline is now
taken only after a fresh refetch, and each attempt opens a unique popup so a
leftover window is never reused. - Qoder parallel tool echoes are recovered, not leaked. When Qoder echoes
several tool calls as text ([assistant 请求调用工具] …JSON) with
unescaped inner quotes, the whole block used to be dropped and shown as
visible text. Multi-call echoes now salvage into real tool calls, and the
same recovery applies to the thinking channel (reasoning_content), so
DeepSeek-planned parallel batches run instead of leaking. - Routing no longer dials a cooling account through continuation affinity.
A multi-turn request carrying tool results was pinned to the account that
owns the live session before health was checked, so a session whose
account had hit quota kept being dialed and reproduced the refusal — the
"still hit a cooled-down account" report. The pin now evaluates eligibility:
a hard-cooling account answers429with the real reset, and a
disabled/locked one answers a clear400. - Qoder upstream 499 refusals cool the account as quota. Qoder closes an
exhausted-quota stream with an nginx-style 499, which read as a malformed
request (non-retryable, no cooldown) and kept the account in rotation. 499
scoped to qoder now classifies asquota_exceededwith a cooldown. - Qoder OAuth accounts recover from a cold credential cache. The loader
storesauth_stateas JSON text, so the adapter receivedundefinedafter
a cache eviction and refused with "missing its user identity; sign in
again". The state parser now accepts the JSON-string form too. - Qoder tool-protocol echoes fail closed instead of leaking. When Qoder
echoes a tool-call block ([assistant 请求调用工具] …) that cannot be
recovered — malformed inner JSON, unescaped quotes that do not repair —
the turn now fails with a 502 instead of surfacing the protocol text as
visible content and leaving the model looking successful while nothing ran. - Qoder echo recovery holds long, fragmented streams. A valid echo split
across hundreds of tiny stream deltas used to trip the 200-frame hold cap
and was released as visible prose — the leak. The cap now counts only
frames that add no bytes, so a growing echo is held until recovery, and
salvaged arguments are validated (malformed arguments are rejected, not
passed through). - Qoder tool-style tag echoes are recovered. The model sometimes echoes a
tool call as an XML-like tag (<execute code="..." />) instead of a JSON
array. These now recover into structured tool calls instead of leaking as
visible text. - Qoder wrapped
<tool_call>echoes are recovered. The model also wraps
each echoed call in<tool_call>…</tool_call>blocks; the stream hold-back
now keeps tag-style candidates (not just JSON) so a long, fragmented wrapped
echo is recovered instead of released as visible text. - Qoder XML-style echoes use one generic parser. Self-closing tags
(<execute code="…" />), paired blocks (<tool_call>…</tool_call>), and
angle-wrapped JSON (<{…}>/<[…]>, including a double-brace artifact)
are handled by a single parser, and the stream hold-back keeps angle-wrapped
candidates until their closing>so long fragmented echoes recover instead
of leaking.
Cartethyia 2.7.1
Cartethyia 2.7.1
[2.7.1] - 2026-10-28
Fixed
-
Qoder: index-less continuation fragments and plain-text leak markers keep
their arguments. Two upstream shapes still dropped tool arguments after
the first fix: continuation chunks that omitindexentirely (no pinned id
to join, so encoders dropped the nameless fragment), and the INTL leak
marker[assistant 请求调用工具] …when it arrives as plain text rather than
DSML markup (it was never buffered, so it never reached recovery). The
translator now falls back to the most recently seen call id for index-less
fragments (mirroring the generic chat parser), and accumulates non-DSML
text so the leak marker is recovered at end of stream with full arguments —
the "assistant talks, then calls a tool with {} arguments" reports. -
Routing no longer redials an account whose cooldown deadline has elapsed
but has not been swept yet. A row still incooldownstatus used to read
as healthy the moment itscooldown_untilpassed, so requests landing in
the window between the deadline and the next 30s recovery sweep reproduced
the refusal that cooled the account — logging a freshcooldown → cooldownhealth row for each one (the "still hit a cooled-down account"
report). The snapshot builder now stamps acooldownrow regardless of
whether its deadline is expired; the sweeper remains the only exit, so the
account is not served again until it is genuinely recovered. -
Qoder billing-limit refusals no longer invalidate the credential. Qoder
answers an exhausted daily billing allowance with
{"code":"110","message":"Billing daily count exceeded"}on a 403 wrapper.
The status-only fallback read the 403 as a credential fault, so a healthy
account was parked asauth_invalidated— the console's "Re-login
required" — and operators were sent to re-authenticate a credential that was
never rejected. The110code and the "Billing daily count exceeded" phrase
now classify as quota (an account cooldown), and unknown numeric provider
codes are never treated as credential evidence — the safe unclassified
cooldown is the default for a code we do not recognise. -
Qoder streaming tool calls no longer lose their arguments. The upstream
identifies one streamed call by OpenAI chunk shape —id+namein the
first chunk, then only the wireindexon every continuation fragment. The
qoder translator synthesized a freshcall_<index>id per chunk instead of
pinning the first id, so every continuation arrived nameless under an
unestablished identity and was dropped by the encoders. Clients saw tool
calls whoseargumentswere always{}(agent harnesses rejected every
native bash/write/read call as "missing required field"). The translator now
pins the first id per wire index and hands retries the same behavior as the
generic chat parser. -
Capability degradation no longer strips tools as collateral. When the
only capability a route cannot serve was ordered aftertoolsin the
degradation chain (an image attached to a text-only model such as Mercury
3), the surviving cumulative variant had already lost the tool catalog — the
tool-capable model then received a transcript of[tool:<name>]text
placeholders with no tool definitions and imitated them instead of
structurally calling tools. Degradation now tries each single-capability
loss against the pristine request first, so the smallest surviving
degradation wins (e.g. drop only the image, keep tools) before the
cumulative chain runs as fallback. -
Trae stream refusals are classified, not blanket-routed. Upstream
1001 "we are not able to authenticate you"now surfaces as an account-scoped
authentication_failed(401 + credential evidence) instead of a generic 502
platform fault, and4008 "exceeded the quota"asquota_exceeded(429).
Error objects carried in frame bodies (HTTP 200 with{"code":…,"message":…})
are detected too, not only namederrorevents — the wrong classification was
hiding the real cause and could drag the proxy pool into the story. -
The health classifier recognizes Trae's 1001 verdict. A numeric upstream
code was previously excluded from the credential arm, so1001fell through
to theunknownfallback that never mutates — the account stayed in rotation
and was re-dialled on every request despite the upstream's auth verdict. Trae
1001is nowauth_invalidated(account-scoped, routing-visible), so the
account cools/excludes and failover moves to a sibling. -
Trae prompt-cache estimation is re-adopted and scoped per (session,
model). Trae's agent surface never reports cache usage, so repeated
prefixes are estimated locally from per-session history; two models sharing a
conversation id cannot cross-hit, and the assessed usage rides the terminal
event so non-streaming telemetry records real counts. -
Buddy desktop device-token follows the official
token || error
contract. The desktop client sendsX-Device-Tokenon every realm (Intl
included) and never omits both it and the error header: when no real token
exists it sendsX-Device-Token-Error: sdk_not_initialized. The buddy
providers now do the same instead of dropping the header (or sending it only
on CN). -
A first-event timeout is a deadline, not a client cancel. The
empty-stream watchdog used to classify a silent upstream (e.g. a provider
holding the request in a queue) astransport_closed499 — "cancelled by
client" — hiding a real upstream wait behind a cancellation. It is now
deadline_exceeded504 with upstream origin, matching the streaming
stall/first-chunk watchdogs. -
Qoder recovers single-object leak echoes. The INTL leak marker could be
followed by one JSON object ({"name":…,"arguments":…}) instead of an array;
that shape was rejected, so the whole block was released as prose and the
marker leaked to the client as visible text.parseLeakedToolCallsnow
normalizes a single object into a one-entry array.
Added
-
Provider Detail "Activity Matrix" usage card. A collapsible card at the
top of the provider page condenses the last 1–366 days of usage into a
calendar heatmap (Mon–Sun weeks,GET /console/api/providers/:providerId/usage/matrix?days=N, aggregated directly
fromtelemetry_events— no migration). Summary strip totals requests,
tokens, cached tokens, and estimated cost. Cells are tinted by the day's
dominant model with a stable per-model color (hashed hue), hover shows a
native day tooltip broken down per model, and an expandable "Usage by model"
list ranks models by tokens with colored share bars. -
Inception Labs (Mercury 3) as a bundled free-tier provider.
inception
ships withmercury-3pinned to the free tier — one million token context,
131072 max output, reasoning and tool calling, zero price. It appears under
Free Limited Providers in the console. A 429 from the per-minute window
cools the account for one minute (upstreamRetry-Afterstill wins) instead
of the generic fifteen-minute guess, so a throttled account recovers as soon
as the window resets. -
A
randompool-group selection strategy. Pool selection previously only
knew the ordered scans, so a burst always hit the same first pools and
burned them one by one while siblings sat idle — the "all pools cooling,
fresh ones unused" pattern.randomshuffles the eligible pools per request
(still skipping cooldown/blocked pools and falling through full ones). Set
it per tenant via the pools strategy endpoint; migration0025ships for
both backends. -
Pool cooldown and probe verdicts survive refresh and restart. A probe
429 now writes a cooldown marker the console shows, markers persist across
restarts, and blocked egress verdicts are restored at boot — a spent or
retired egress is not re-picked after a restart. -
Qoder daily check-in (streak / 100 Credits per day). Qoder accounts now
ride the same daily check-in machinery as the Buddy family: the quota sweep
claims the grant once per day, and a manual trigger is available through the
account quota surface. Legacy/sash/.../daily-check-instatus/claim is
tried first; when the international realm does not expose it (404), the
CREDITS-kind activity campaign is claimed instead. Streak days and the 100
credit grant are reported in the sweep log. -
OpenCode free tier aligned 1:1 with the reference bridge (v2). Flat
/inferencedispatch paths, the minimal free-tier prompt, native proxy
egress, and pool bulk actions in the console. -
Validated direct egress uses native fetch by default, with per-provider
opt-in and opt-out. -
The TuringShield SDK is bundled under
vendor/turing-sdk/(WorkBuddy
desktop 5.8.4 extract) so buddy providers generate realX-Device-Token
values without any environment setup —CARTETHYIA_TURING_SDK_PATHremains
an override. Per-provider payload profiles (WorkBuddy vs CodeBuddy product
identity on channel 400111) make tokens differ between providers, and the
Dockerfile ships the vendor directory into both builder and runtime stages. -
Per-account device binding (
qimei36). Buddy desktop chat now sends
X-Device-Id-List: {"qimei36":"…"}resolved per account — explicit
auth_state.qimei36wins, otherwise a deterministic sha256 of the account id
(32 hex, stable across restarts, no DB write). Every pooled account presents
a distinct device alongside the machine-bound Turing token. -
Mistral catalog expanded to models verified live. The static roster was
re-checked against a live key:codestral-2508/latest,
mistral-code-latest, the ministral family,voxtral-small-2507/latest,
...
Cartethyia 2.7.0
Removed
- The process-wide concurrency cap. Admission now governs in-flight requests alone: the tenant ceiling and each key's own limit, both set in Settings. The second ceiling was derived from the memory limit, was never a number an operator set, and was the smaller of the two — so a tenant configured to 100 was refused by a constant the Settings page never mentions.
CARTETHYIA_PROCESS_CONCURRENCY_LIMITis gone. - The model-abuse strike and ban layer. Ten invalid-model requests inside thirty minutes banned a caller for six hours with
403 model_abuse_banned, by a counter no setting exposes and the rate ceiling does not interact with. Removed, along with the fourCARTETHYIA_MODEL_STRIKE_*/CARTETHYIA_MODEL_BAN_*knobs.
Fixed
- The memory limit no longer pretends every host without a cgroup is a 512 MiB container; the fallback is the host's real memory.
- Qoder tool calls carry their arguments: tool history is sent in the shape INTL reads, messages carry
{role, content}only,model_configdeclaresformat: "openai", and a namedtool_choiceis a bare string. - Qoder recovers a call written into its thinking, including leftover closing tags without the
DSMLmarker, and salvages an echo whose inner quotes were left unescaped.
Added
- The in-flight snapshot reports
oldestAgeMs, so a growing age beside a flat count shows a sweep that is not running. cartethyia_ip_abuse_refusals_totalrecords why the abuse layer refuses.
Changed
- Tailscale requires a live serve before reporting a tunnel active, and names the address or says why there is none.
npm: npm i -g cartethyia@2.7.0
Cartethyia 2.6.7
Fixed
-
One stalled lease no longer stops every scheduled task.
runOnce
latched the process-widebusygate and then awaitedlease.tryAcquire
with no bound. A lease store that never answered leftbusyset forever,
and because the gate is shared across every task name rather than
per-name, every later tick of every task returned at it: the whole
registry stopped running. That includedinflight-backstop, whose entire
job is to clean up after something that never finishes — so one stalled
acquisition is what let stale in-flights sit until restart.The acquisition is bounded at 5s and treated as a skip, which is what
tryAcquirealready documented without enforcing. Skipping releases the
gate, and every path that latches it without running the task now goes
through onereleaseGate. -
A banned buddy account is disabled again. The ban detector matched on
the message, but the provider's message for a ban is the same two words it
uses for a refused request: "request illegal". The actual verdict ("Your
account has been restricted due to unusual activity") lives in
displayMsg, which the message extractor never reads, so the ban was
never detected: nopolicy_blocked_banned, no disable, and the account
kept serving. The body is now read too — fromupstreamBodyor
details.raw— including each language of thedisplayMsgmap and an
appeal-bearingactionsentry.
Cartethyia 2.6.6
[2.6.6] - 2026-10-27
Fixed
-
Hide to tray no longer exits while the gateway is still serving. The
tray polled/healthevery 10s and closed itself after 3 consecutive
misses, with a 2s probe budget. A gateway that is answering, but slowly,
misses every probe exactly like a gateway that is gone: under a large
prompt or a synchronous GC,/healthcannot answer inside 2s, so three
polls in a row missed and the icon exited while the gateway was still
serving.carte --traylooked fine only because it starts idle and never
carries that load.The miss count is now a wall-clock window: a gateway that is truly gone
stays unreachable and still closes the tray promptly, while a gateway that
is merely busy answers often enough to reset the clock. The probe budget is
15s — far longer than a healthy answer takes, far shorter than the poll
interval.
Added
- Tests for the in-flight backstop sweep, which had none. The sweep is
the only thing that reaps a flight whose finalizer never ran. These pin
that a flight past its deadline plus the hard grace is released, that one
inside the window is not, that a deadline moved by a live stream is
followed, and that reaping is idempotent.
Cartethyia 2.6.5
[2.6.5] - 2026-10-27
Fixed
-
A synchronous GC ran while requests were in flight.
main.tspassed
every dependency tocreateGatewayAppexcept the runtime sampler, so the
app'sbindInFlightcall no-op'd on an optional dependency and the sampler
kept its() => 0placeholder.cartethyia_process_in_flightreported zero
forever whileproxy_in_flight— the registry the placeholder was meant to
read — reported real flights.checkIdleReclaimgates oninFlightSource() > 0, so a permanently-zero
source told the gateway nothing was in flight and let it run a synchronous
Bun.gc(true)under load. A synchronous collection stalls concurrent
requests: latency pinned to the request deadline and clients cancelled en
masse with 499.Both classes behaved exactly as written — only the wire between them was
missing — so it is pinned by a composition test that readsmain.tsand
requires the field.
Cartethyia 2.6.4
[2.6.4] - 2026-10-27
Fixed
-
A full max-inflight is no longer an account rate limit.
provider_capacity_exhaustedis raised by this gateway's own routing when
every account serving a model is at its max-inflight — the credential is
fine, the ceiling is what rejected the request. But it is a 429 whose text
says "rate limit", so it matched the rate-limit arm and cooled the account
for the full 15-minute window. Every account then readcooldownat once,
so all of them looked busy while requests piled up. Now classified as
capacity with zero cooldown and no account penalty. -
A refused CONNECT tunnel is a pool failure, not an account failure.
Proxy CONNECT failed: 403 Forbiddenfell through to the unclassified
provider arm, which cooled every account behind that pool — so one dead
front door on one provider took unrelated providers down with it. Now
scoped to the pool.
Changed
-
policy_blockedsplit into a ban and a request refusal. One category
served two shapes needing opposite remedies:11140whose body names a restriction on the account ("Your account has
been restricted due to unusual activity") is now
policy_blocked_banned. It disables immediately with no window — and no
window is what keeps it disabled, since the recovery sweep only revives a
disabled row whose deadline has elapsed.11-128and safety reviews are nowpolicy_blocked_invalid_request.
The credential is fine, so this mutates nothing: the only correct move is
to stop retrying verbatim.
Told apart by the body, not the code alone, and gated on the buddy family,
so an unrelated upstream saying "restricted" cannot disable accounts. -
The usage floor shows inactive accounts separately. "Pooled credits"
counted active accounts only and silently dropped the rest, so the pooled
number looked wrong with nothing to explain it. The inactive count is now
shown alongside.
Added
-
The accounts toolbar is flat. The bulk actions (Recover, Enable,
Disable, Export, Delete) had lived behind an "Actions" chevron, so most of
what the toolbar could do was invisible until accounts were selected. They
are now persistent buttons that grey out when they do not apply, so the
toolbar's shape never depends on what is selected. -
A static JWT's issuer is shown on the account health modal, under
Credential management. Read from theissclaim at registration, since the
secret is unreadable afterwards. Only shown when the payload carried the
claim. Accounts registered before this release show no issuer until re-added.
v2.6.3 — Buddy identity hardening and tenant-safe restores
Release 2.6.3
- feat(buddy): harden CodeBuddy/WorkBuddy identity against the reference gateway
- fix(console): remap restored rows to the restoring tenant
- release: 2.6.3 - Buddy identity hardening and tenant-safe restores
Cartethyia 2.6.2
[2.6.2] - 2026-10-26
Fixed
-
The update and already-running paths can be read. Two exits happened too
fast to be read, and both made a failed action look like a successful one.An update that could not install still printed "this process will exit now"
and exited, so a version that never changed looked updated. The notice now
names what actually blocks the install — npm replaces the package by
renaming its directory, which Windows refuses while any process holds it,
and the hold can outlive the process that took it — and prints the command
to run by hand. The menu holds the notice for 30 seconds instead of
exiting, and the installer stopscartethyia/cartefirst so the rename
has a chance.A second instance reported "already running" and exited immediately, so a
double-clicked shortcut or a re-run in a terminal that is already serving
closed the window before the reason could be seen. It now waits 30 seconds
before closing.