Replayed tool calls only carry extra_content for providers that opt in
(Gemini), so OpenAI-compatible and Responses vendors don't see an unknown
field. The native Gemini client now owns its request deadline: the SDK's
own timeout aborted without a reason and read as an internal error. A
deadline timeout returns 504 upstream_timeout and leaves the route in
rotation.
A 403 model_not_found still returns 400 upstream_bad_request, but now
alarms at info so a lost provider entitlement gets noticed. HttpError
gains an alarmSeverity option the terminal alarm gate honors.
* feat: bound provisioning by what an owner may mint, not only what is live
Seat and team caps express how much may exist at one time. This adds the
other half of the rule: how much an owner may create over a window, and the
checks a provisioned account answers to.
- Provisioning puts each seat through `SignupService.runSignupGate`, the same
extension-owned gate a signup answers, and refuses what it blocks. It fails
closed, since a check that could not run is not a check that passed.
- A per-owner budget over a rolling window, sized to every seat the owner may
hold live across every team they may own, times the churn allowed on top.
It is spent on creation and is not returned.
Keyed on the owner's uuid so a recycled id cannot inherit someone else's
spend, and charged after validation so a taken username costs nothing.
* refactor: tighten the provisioning gate and budget after review
- The gate runs before the seat-cap lock. A listener has no deadline and the
lease is ten seconds with nothing to renew it, so the lock must not be held
across one.
- `failOpen: false` rethrows rather than returning false, so a backend outage
is answered 503 instead of escaping the handler.
- An address goes through `validateEmail` first, as it does on every other
path that creates an account.
- The gate always runs; a request only enriches what it is told, so one being
absent is no reason to skip it.
- The gate is told it is looking at a seat, whose address is optional.
- A refusal carries the gate's own code and request code, as a blocked signup
does, so support can correlate it.
- Restored `provisionAccount`'s docblock, which an insert had displaced.
Tests: the spy restores in `finally`, and the cap overrides are taken after
the fixtures, so one failure cannot cascade through the file.
* fix: show a provisioning refusal as a rule, not a failure
The console special-cases the seat cap and sends everything else to
"Something went wrong", which reads as a fault rather than an answer. The
provisioning budget refuses the same way the cap does, so it is presented
the same way.
* fix: count one app open per account per app per day
`app_opens` is read two ways at once: `open_count` is a row count and
`user_count` is distinct accounts. A repeated open moves the first
without the first moving the second, and the incentive report prices
opens.
An open is now marked as it is recorded and skipped for the rest of the
day, before both the row and the event ClickHouse ingests, so the two
stores agree. The marker is set only when absent, so the first open of
the day wins a race; an unreachable marker counts the open, since
counting twice beats not counting.
* fix: count one app open per account per app per day, at the count
Dropped the row-suppression approach. `app_opens` is not only a counter:
`getRecentAppOpens` orders the desktop's recent list by `MAX(_id)`, and
`app.opened` is the socket event other tabs reorder on. Suppressing the
repeat row froze both at the first open of the day.
Every open is recorded as before. `open_count` counts each account once
per day instead, which is the figure `user_count` is read against.
`countDistinctUserDays` folds the pair into one number because
`COUNT(DISTINCT a, b)` is MySQL-only, and `dayBucket` covers MySQL's
decimal division. ClickHouse has uniqExact over the tuple.
Note the figure's meaning changes at deploy: a period spanning it mixes
rows with account-days, so a comparison across that point is not one.
* fix: count one app open per account per app per hour
Review feedback: a day is too long a window for the repeat to collapse
into. The period is now an argument rather than a baked-in 86400, held in
one constant on AppStore beside the other store policy numbers, so the SQL
and ClickHouse sites cannot drift and a future change is one line.
The fold `COUNT(DISTINCT user * N + bucket)` is only injective while the
bucket index stays under N. Hourly buckets are 24x the day index and pass
the old 1000000 in 2084, so N moves to 10000000.
Buckets are clock hours, not a rolling window, so two opens a minute apart
either side of the hour still count twice — tested, and the same property
the day version had at UTC midnight.
The detailed-stats fixture gave its repeat open a 60s offset, which under
hourly buckets would have counted twice whenever the suite ran in the first
minute of an hour. It now shares a timestamp with the open it repeats.
- relay sign-in popups take the opener origin from the referrer only
- session revokes read the primary, batch per user, and tombstone caches in every region
- wisp relay tokens bound to the minting session; verify rechecks the account
- hosted origins keep app identity with the site owner; deleted app uids recorded in deleted_apps
- subdomain cache tombstoned on delete, update, reclaim and app/account cascades
- email lookups require an exact address match after the SQL lookup
- workers deploy only from files the deployer owns
- share_app_data:false enforced at ACL time, plus copy/move/rename/share/recursive listing
- kv remove returns no value without read; kv:expire op grant usable
- fs errors name the entry instead of the owner's path
- shared-by-me drops rows a delegate no longer has authority over
- private apps can't move index_url onto another user's origin
- GUI: default apps saved and launched by uid; save picker accepts only copy/move; permission dialog waits for focus; Publish as Worker hidden on others' files
* fix: give every route behind the user-protected gate a password budget
Five of the six routes behind the gate declare a per-user hourly bucket
for the password check. The sixth, delete-own-user, declared none.
The bucket moves into the gate itself, so it covers every route behind
it and a new one cannot be added without one.
It is charged by the outcome rather than by the request: a wrong answer
spends one and a right answer spends nothing, so a caller who knows the
password never meets the limit.
* fix: the review's findings on the password budget
Reading the budget and spending it on the outcome bounded rounds rather
than attempts: everything already in flight read the same budget and was
answered before any of it was spent. The charge lands before the
comparison now, so a burst is bounded by the same number as a sequence.
The key gained a route component. One bucket across all six meant ten
mistyped passwords on one route closed the other five for the hour,
including the one an account owing its team's 2FA has to reach.
getObjectStream now attaches an 'error' listener to every body it returns.
A body that fails before its consumer attaches (socket timeout, reset, or a
checksum mismatch on a truncated read) used to emit 'error' with no listener
and exit the process, failing every in-flight request on the task. It now
logs a warning; consumers that attach later still receive the error.
* PUT-2012 fix broadcast service drops
* Revert pre-commit lint churn on files merged from main
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix test bug
* Remove other test
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* perf(auth): renew godmode app tokens from cache instead of the primary
Every godmode launch read its token row from the primary and rewrote its
expiry, a few cross-region round trips per launch far from the primary.
- A cache pointer names the row per (user, web session, app). The row is
read through the uuid cache token auth uses and re-checked (owner,
session, app, revoked, expired, max age); anything else falls back to the
primary read. Revoking the row drops the pointer.
- The expiry write is skipped when it would move by under an hour. A
renewal that hits a revoked or lapsed row mints a new one instead.
- The response carries the row's actual expires_at.
* perf(gui): reuse a just-minted godmode token for repeat launches
A launch of a godmode app reuses the token this page minted for it within
the last ten minutes under the same desktop session, instead of asking
again. The cache is dropped when an app reports its token dead, after a
session revoke from the session manager or an uninstall, and across tabs
via a BroadcastChannel that carries token row ids, never tokens. A mint in
flight during a drop is not remembered.
Add interactive-widget=resizes-content to the mobile viewport meta tag
so the layout viewport shrinks when the soft keyboard opens. The app
window's height chain resolves against the layout viewport, so the
iframe folds up above the keyboard instead of clipping behind it.
- TTS sends output_format as the query parameter ElevenLabs reads; the body
field was ignored, so every request came back as MP3. Unknown formats are
rejected before credits are reserved, and the OpenAI-style names mp3, opus,
pcm and wav map to native formats.
- Voice IDs go straight to synthesis. The library listing omits voices that
still synthesize, the default 21m00Tcm4TlvDq8ikWAM among them, and runs to
9 MB per call. Names resolve through /v2/voices search, and premade voices
("Bella - Professional, Bright, Warm") also match their short name.
- speech2speech accepts HTTPS audio URLs through secureFetch, as documented.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Catalog gemini-nano-banana-2.1 (aliases google/gemini-nano-banana-2.1 and
nano-banana-2.1) at Google's standard rates: $1.50/1M input, $7.50/1M text
and thinking output, $30/1M image output, with 1K/2K/4K tiers at
1120/1680/3780 image tokens. 3.1 Flash Image stays the provider default.
Pre-flight credit checks assumed 50 text/thinking output tokens per image
for every Gemini model; live samples run 270-1122 depending on the model, so
each model now carries its own estimate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>