* fix: coerce Mistral image_url parts from object to string
Mistral's API expects image_url content parts to be a plain string URL,
not the OpenAI-style { url: string } object that process_input_messages
produces. Without this coercion, sending an image to any Mistral model
that supports vision (mistral-small, mistral-medium, mistral-large,
ministral-*) results in a request error from the Mistral SDK.
Add #coerceImageUrls() as a private method on MistralAIProvider that
maps { type: 'image_url', image_url: { url } } -> { type: 'image_url',
image_url: url } for every content part in every message. Messages with
plain string content are left untouched, as are parts whose image_url is
already a string.
Add four unit tests covering: object-to-string coercion, already-flat
strings, plain string message content, and mixed text+image content.
* style: use plain ASCII dashes in comment section divider
* feat: worker sessions get their own kind + per-(user, app, worker_name) row
Schema
------
- mysql_mig_11.sql + sqlite 0054: add idx_sessions_user_worker_active,
a partial unique index over (user_id, app_uid, meta.worker_name) for
kind='worker' rows. Active worker sessions are deduped by that triple
so each named worker gets its own session row and they don't fight
the existing idx_sessions_user_app_active (which still constrains
kind='app' only). app_uid is allowed NULL for user-scoped workers
with no app binding.
SessionStore
------------
- getOrCreateWorker(userId, { appUid, workerName, ... }): mirrors the
getOrCreateApp pattern — cache lookup, partial-unique re-SELECT on
insert-ignore, all keyed on the worker triple. expires_at lands at
WORKER_WINDOW_SECONDS (~99y) so the worker doesn't have to re-mint
on any cadence.
- #cacheKeyWorker + #allCacheKeysForRow worker branch so revoke /
update invalidates the worker cache view alongside the by-uuid one.
AuthService
-----------
- createWorkerSessionToken(user, workerName, meta?) now takes the
workerName explicitly and routes through getOrCreateWorker. Emits
the same { session, token, gui_token } shape but both JWTs carry
{ worker: true, worker_name }.
- createWorkerAppToken(actor, appUid, workerName) likewise — JWT
carries the worker_name claim so a verifier can tell two workers
under the same app apart without a DB round-trip.
- Both methods 400 on empty workerName.
WorkerDriver
------------
- Five auth-mint call sites swapped over: app-bound deploy (3x:
appId branch, actor.app fallback, hot-reload redeploy), user-bound
fallback (2x: cold deploy, hot-reload). All pass `workerName` so
the worker's session row is naturally idempotent across redeploys.
GUI manage-sessions
-------------------
- sessionTitle adds a kind='worker' branch ("name (app)" for
app-scoped workers, just "name" for user-scoped), pulling worker_name
from the meta-spread that listSessions already surfaces.
- en.js adds ui_session_kind_worker.
* fix(workers): MySQL JSON_EXTRACT quoting + revoke cache invalidation +
SQLite NULL-distinct in worker index
Three real bugs in the worker session plumbing from the prior commit,
plus a misleading comment. Schema design kept (worker_name lives in
`meta` rather than a dedicated column) per offline review:
1. `#selectWorkerRow` compared `JSON_EXTRACT(meta, '$.worker_name')`
directly to a bind parameter. MySQL's `JSON_EXTRACT` returns a
JSON-typed value with embedded quotes (`"name"`, not `name`), so
the comparison never matched. After the first INSERT, every
follow-up getOrCreateWorker call missed the existing row in the
SELECT, hit INSERT-IGNORE, then missed again in the re-SELECT —
the caller would receive whatever the INSERT-IGNORE returned (a
no-op row in conflict cases). Wrap with `JSON_UNQUOTE` on MySQL
via `db.case`; SQLite's `json_extract` already returns the
unwrapped scalar so it keeps the literal form.
2. mig_11's generated `worker_unique_key` had the same JSON-quoting
bug. Mirror the fix: `IFNULL(JSON_UNQUOTE(JSON_EXTRACT(...)), '')`
so the concatenated unique key is a plain string that lines up
with what `#selectWorkerRow` now binds against.
3. SQLite UNIQUE indexes treat NULL columns as distinct (per the SQL
standard), so two user-scoped workers (app_uid NULL) with the same
worker_name would both insert. Wrap the index expression with
`IFNULL(app_uid, '')` so they correctly conflict — matches the
MySQL side's `IFNULL` in the generated column.
4. `removeByUuid` / `revokeCascade` SELECTed only the identity
columns (no `meta`), so `#allCacheKeysForRow`'s worker branch
couldn't read `meta.worker_name` and the composite
`sessions:v2:worker:<user>:<app>:<name>` cache key survived
revocation. Up to CACHE_TTL_SECONDS (15min) afterwards,
getOrCreateWorker would short-circuit to the cached (revoked) row.
Add `meta` to both SELECTs; the existing meta-parsing logic in
`#allCacheKeysForRow` handles the rest.
- decrease ai related alerts and make errors not 500
- decreased bad fs controller checks to be 404s instead of 500s
- force bucket regions for now
- validate webdav perms for locking/unlocking
xAI's Voice pricing table lists Text to Speech at $15.00 / 1M characters,
but we were charging $4.20 / 1M characters (420 microcents/char), undercharging
by ~72%. Update the cost constant, listEngines pricing, and test expectation
to 1500 microcents/char.
Adds offline XAITTSProvider.test.ts covering voice/format selection,
request shape, error paths, and cost reporting. Spies on global fetch
(the provider's egress point) against a real PuterServer + live
MeteringService.
Closes#2998
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds offline ElevenLabsTTSProvider.test.ts covering voice/format
selection, request shape, error paths, and cost reporting. Spies on
global fetch (the provider's egress point) against a real PuterServer
+ live MeteringService. The companion integration test stays untouched.
Closes#2999
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>