Skip to content

Tale v0.5.24

Choose a tag to compare

@larryro larryro released this 14 Sep 00:58
84edd90

0.5.24 is a fix release on the 0.5 line that carries more than its fixes: the fifth set of REST contract corrections from an external API evaluation (contract 1.5.0 → 1.6.0), JSON refusals at the edge where a machine caller used to get the HTML maintenance page, a reply cap that bounds a whole chat turn, and a repaired managed on-premises deployment path in the CLI. Two platform migrations (0100, 0101) apply themselves on tale deploy, each an additive nullable column written to run while the previous image is still serving. No knowledge-database migration, no configuration-file change and no new environment variable. The proxy image changes and a plain tale deploy does not roll it — see Upgrading. The Known issues from 0.5.20 are unchanged; the two verification gaps 0.5.23 listed are closed, and this release's own are listed with them.

Highlights

The fifth API evaluation pass (#3335)

The fifth external black-box evaluation of the REST and MCP API ran against 0.5.23 (~1,800 requests, no backend 5xx) and reported 0 S1, 2 S2, ~13 S3 and ~15 S4 findings. Every item was second-confirmed from source before a fix was designed. The headline S2 — an intermittently slow or failing TLS handshake over IPv4 while IPv6 was clean — was the evaluating machine, not the platform: a tunnel interface on that host captured every locally originated IPv4 flow while IPv6 left directly, and bound to the physical interface the IPv4 handshake was as fast and as reliable as IPv6 (40 of 40, about half a second). No platform change. The second S2, a listed model the provider's plan did not cover, was closed in the operator's model allowlist during the round, where 0.5.23's notes had placed it. The evaluation also drove the project-chat scope 0.5.23 shipped on live model turns — a project thread reads its project's file, the organization thread stays on the hub — which closes the verification gap those notes listed. What did hold is fixed here; every wire change is listed under API contract changes.

  • Edge. A truncated HTTP/2 upload — a declared Content-Length larger than the bytes sent — made the edge answer the HTML maintenance page with a 502, under the body cap too, so the platform never saw the request; it now answers 400 BODY_LENGTH_MISMATCH in the JSON envelope. While the platform restarts, a machine door (/api/*, /scim/*, /http_api/*, /events, /status.json, /openapi.json, /.well-known/*) gets a JSON UPSTREAM_UNAVAILABLE with the error's own status (502, 503 or 504) and Retry-After: 5, where a browser navigation still gets the maintenance page. A HEAD is never compressed, so it reports the uncompressed Content-Length the GET would carry (the encoder used to stamp the length of an empty gzip frame). The dot-segment rule also refuses a raw URI whose dots fold into the API prefix (/x/../api/v1/me), which the backend's own parser folded and served.
  • Door. Idempotency-Key is printable ASCII, enforced: any other character answers 400 INVALID_HEADER on run starts and chat sends (the pattern was declared, not enforced, so two spellings of é named two starts of one retry). Starting a task's workflow answers 404 AUTOMATION_NOT_FOUND or 409 AUTOMATION_NOT_DEPLOYED where it answered 200 {started: false, reason: "not_started"}. The upload mint and the bind require a file name that ends in an allowlisted extension whatever contentType declares — CON, an extension-less attachment-4711 and program.exe declared as text/plain all got in through the MIME half of the check — and the app's document pickers inherit the rule. File and folder names are stored trimmed and NFC-normalized; FOLDER_NAME_INVALID names the rule broken and carries data.issues. A website search limit outside 1..100 is refused with 400 INVALID_BODY instead of clamped. HEAD on /api/health, /status, /status.json and /openapi.json carries the GET's Content-Length; the two webhook doors answer X-Tale-Api-Version; the app-wide guard refusals (the x-api-key 401, the NUL-in-URL 400) carry Cache-Control: no-store.
  • Chat. maxOutputTokens bounds the whole turn across model rounds: a later tool round gets what the earlier ones left, a round that would start with nothing left is not run, finishReason: "length" marks the reply whenever any round was cut, and a tool call the cap cut mid-arguments is not executed — its result reads status: "invalid_args" (it used to run with whatever arguments fitted, and the next round settled stop over a cut reply). 0.5.23 said locale pins the reply language; the evaluation measured a hint (four German replies out of six). The directive now rides the system prompt, naming the language, and the message itself, where a reasoning model on a short prompt looks; the reference says "asked to answer in" and tells a client that must have the language to check the reply and resend. costEstimateCents is rounded to a millionth of a cent (the rates' binary noise, 0.042601999999999994, reached the wire and the ledger). The assistant is told to search before answering that it does not know a term.
  • Projects, tasks, automations, skills, documents. POST /projects/{id}/files/{documentId}/retry-indexing indexes a REST-bound project file without delete and re-bind — 0.5.23's notes pointed at the Hub document's retry, which answers 404 for a project file. externalState: "open" reopens a task the mirror itself parked at in_review (migration 0101 stamps such a park; a park a person or an agent made stays theirs) — a mirror that closed an item and reopened it upstream used to leave the card parked for good. testsPassed reads false after the deploy gate refused a version (it persisted nothing, so a release pipeline could not tell "no tests" from "the tests fail"), testsCheckedAt says when (migration 0100), and the MCP save_automation records the save's own test run. A run summary carries id beside runId. An identical PUT /skills/{slug} writes nothing — the history trail no longer evicts real versions for a mirror's re-push. A content-only document download honours If-Modified-Since (it re-downloaded every inline document on every poll), and GET /documents/{id}/content declares its conditional headers and 304.

Managed on-premises deployments repaired (#3333, #3336)

A managed deployment's backend-local phases — deploy provision, and the native half of deploy export-client — run inside the backend container and import the backend's own database and auth modules, which import packages from the backend's node_modules. They ran as the compiled executable, which resolves bare imports against its embedded filesystem, so they failed with Cannot find package 'postgres', swallowed twice and surfaced as an operator email attestation failure (or an export failure), which sent debugging the wrong way. Every managed bundle now ships an interpreted build of the CLI, cli/tale.mjs, beside the executable under the same manifest hashes, and both phases run under the container's own bun, so the imports resolve the way the running backend's do. The CLI also requested the provider-credentials collection with a trailing slash the backend does not serve (a 404); the three collection calls use the slashless route. Both were verified live on a real on-premises backend and locked with regression tests.

Behaviour changes

  • A 502, 503 or 504 at the edge answers JSON to a machine door and the maintenance page to a browser; a client that parses JSON no longer meets 4 KiB of markup mid-roll. A body shorter than its declared length over HTTP/2 is a 400, not an outage.
  • A HEAD through the edge is never compressed; its Content-Length is the uncompressed length.
  • A file name without an extension, or with one outside the allowlist, is refused at the mint, at the bind and in the app's upload and replacement pickers, whatever MIME type is declared; a declared type never stands in for the extension.
  • File and folder names are stored trimmed and NFC-normalized from now on; rows written before keep their bytes (no backfill).
  • Starting a task's workflow on a missing or undeployed automation is a refusal, not a 200 that started nothing; reason: "not_started" is left for a deployment withdrawn between the check and the start.
  • A tool-calling chat turn spends maxOutputTokens round by round, a cut call is not run, and length is reported when any round was cut.
  • A REST send with locale carries the reply-language directive on the system prompt and on the message; the app's chat is unchanged.
  • Re-pushing an unchanged skill bundle through PUT /skills/{slug} leaves etag and updatedAt alone and writes no history entry.
  • A website search limit out of range is refused, not clamped.
  • An external open reopens a task the mirror parked; any move through the board's own doors ends the mirror's claim on that park.
  • The deploy gate persists a failed test verdict; the latest verdict wins over the one recorded at save time.
  • An Idempotency-Key outside printable ASCII starts nothing.
  • costEstimateCents on a chat message and in the usage ledger is rounded to a millionth of a cent.

API contract changes

The OpenAPI document moves to 1.6.0; every response the platform answers names it in X-Tale-Api-Version. A refusal answered at the edge (the dot-segment 404, BODY_LENGTH_MISMATCH, UPSTREAM_UNAVAILABLE) carries a fresh requestId of its own and no version header: only the platform knows the contract it implements.

Changed

  • Idempotency-Key (run starts, chat sends) — a value outside printable ASCII (0x20–0x7E) answers 400 INVALID_HEADER, the header named under data.issues; nothing starts.
  • POST /projects/{id}/tasks/{taskId}/start — a workflowSlug that names no automation answers 404 AUTOMATION_NOT_FOUND; a saved-but-undeployed one 409 AUTOMATION_NOT_DEPLOYED (was 200 {started: false, reason: "not_started"}), judged before the execute budget is charged.
  • POST /projects/{id}/uploads and POST /projects/{id}/files — a fileName without an allowlisted extension answers 400 UNSUPPORTED_FILE_TYPE whatever contentType says; fileName is stored trimmed and NFC-normalized.
  • POST /projects/{id}/folders — name is stored trimmed and NFC-normalized; 400 FOLDER_NAME_INVALID carries data.issues naming name and a sentence naming the rule.
  • POST /websites/{id}/search — limit outside 1..100 answers 400 INVALID_BODY (was clamped).
  • POST …/messages — maxOutputTokens bounds the whole turn; finishReason: "length" whenever any round was cut; a cut call's tool-result part reads status: "invalid_args"; locale is a directive on the system prompt and on the message; costEstimateCents is rounded to a millionth of a cent.
  • PUT /skills/{slug} — a composed SKILL.md byte-identical to the stored one writes nothing: 200 with the stored etag and updatedAt, no history entry; If-Match and If-None-Match: * are still evaluated first.
  • GET /documents/{id}/content (content-only document) — If-Modified-Since is honoured against Last-Modified at whole-second precision; If-None-Match decides alone when both travel.
  • HEAD — never compressed through the edge (uncompressed Content-Length); on /api/health, /status, /status.json and /openapi.json it carries the GET's Content-Length.
  • POST /api/automations/webhook/{token} and the project twin — every response carries X-Tale-Api-Version. The x-api-key 401 and the NUL-in-URL 400 carry Cache-Control: no-store.
  • GET /automations/{name}/versions — testsPassed reads false after the deploy gate refused a version (was null); the latest verdict wins.
  • Raw dot-segments — a URI whose dots fold into the API prefix (/x/../api/v1/me) is refused at the edge with 404 NOT_FOUND like one whose dots fold out of it.

Added

  • POST /projects/{id}/files/{documentId}/retry-indexing — 200 {status: "indexing"} (lifting the bind-time skipRagIndexing opt-out) or {status: "skipped", reason}, under the same 10-per-user-per-minute budget as the Hub document's retry (429 RATE_LIMITED).
  • AutomationVersion.testsCheckedAt and Automation.testsCheckedAt — epoch milliseconds when testsPassed was judged, null with it.
  • RunSummary.id — the run id beside runId, on the REST listings and MCP list_runs.
  • POST /projects/{id}/tasks — externalState: "open" reopens a task the mirror parked at in_review, or a done one, to backlog.
  • 400 BODY_LENGTH_MISMATCH — answered at the edge for an HTTP/2 body that ended before its declared Content-Length.
  • 502, 503, 504 UPSTREAM_UNAVAILABLE — answered at the edge on every machine door while the platform cannot be reached, with Retry-After: 5.
  • GET /documents/{id}/content declares If-None-Match, If-Modified-Since and its 304.
  • Error.code gains INVALID_HEADER, BODY_LENGTH_MISMATCH and UPSTREAM_UNAVAILABLE.

Documented, unchanged on the wire (en, de, fr): the 64 KiB header budget allows a few KiB of slack on HTTP/1.1 (a 66 KiB URL still answers 414); a never-bound automation runs in any project the caller can edit; an automation's runs outlive its deletion; GET …/triggers answers triggers, a list of at most one; the duplicate 409 codes are named (CONTACT_DUPLICATE_EMAIL, CONTACT_DUPLICATE_EXTERNAL_ID, DUPLICATE_PRODUCT_NAME, DUPLICATE_PRODUCT_EXTERNAL_ID, KNOWLEDGE_ENTRY_DUPLICATE, PROJECT_DUPLICATE_EXTERNAL_ID); a %00 in the URL is INVALID_URL ahead of every route; 422 SKILL_MALFORMED is about the bundle already stored, never the JSON body you send; KNOWLEDGE_ENTRY_STORE_TIMEOUT; a keyless request to an unknown /api/v1 path answers 401 before 404; the webhook door takes POST only and answers its 404 to every other verb; GET /models omits maxOutputTokens when the catalog declares no ceiling; a model failure's error is the provider's own answer prefixed with its HTTP status, so branch on errorCode; similarity is a scale, not a calibrated confidence; the Own Compose page explains IPv6 behind a Docker-published port and trustedProxies.

Known issues

  • Unchanged from v0.5.20, where each is described in full: the es/co-cc Colombian cédula detector still ships switched off and a locale-agnostic PII toggle still widens national-ID matching to every locale (the PII library is untouched in this range); thinking-block replay on the native Anthropic connector is not done and the live Max-plus-tool-call check is still owed; rag_search embedding calls inside a harness turn are unmetered; the product edit dialog cannot clear a field; the app's skill editor still carries the retired private visibility.
  • The x-tale-pagination extension is a declaration on the OpenAPI document; generated clients that do not read vendor extensions still branch on the two cursor names until cursor is retired.
  • The proxy changes were exercised through the real entrypoint in a local Caddy 2.11 container (a short HTTP/2 body over and under the cap, the platform down, HEAD with gzip, the dot-segment spellings), not on a running deployment's edge.
  • A reply-language directive is a directive: a model may still answer in the prompt's language (most often a reasoning model on a short prompt), and nothing on the wire marks a slip.
  • There is no single-file read on /projects/{id}/files: a poller waiting for one file's indexing after retry-indexing walks the folder listing.
  • The app's zip upload of a skill bundle rewrites the bundle and moves updatedAt even when the zip is byte-identical, where PUT /skills/{slug} writes nothing.
  • A tool call the reply cap cut keeps input: {} on the stored tool-call part; the raw text the model emitted is not on the transcript, so the timeline cannot show what was asked.
  • Folder names written before this release keep their bytes; a sync engine's hub-path lookup can create an NFC twin beside a legacy NFD folder. No backfill ships.
  • Behind a Docker-published port, every IPv6 client arrives as the bridge gateway's address and shares one per-address rate-limit bucket and one audit address until the daemon runs with ip6tables and the proxy's network is IPv6-enabled — an operator item, documented on the Own Compose page.

Migration notes

  • Two platform migrations apply on the first boot of the new images, each an additive nullable column with no backfill that the previous image neither reads nor writes:
    • 0100_automations_tests_checked_at adds tests_checked_at_ms to app.automations — when a version's test verdict was reached; null for a version saved without one, and an existing verdict has no known time.
    • 0101_tasks_external_closed_at adds external_closed_at_ms to app.tasks — the stamp of a park the mirror made; an existing park reads as a person's until the next external close stamps it.
  • No knowledge-database migration in this range.
  • The task_labels_project_id_name_key constraint 0.5.22 kept for its rolling deploy is still in place; dropping it is a follow-up migration, not part of this release.
  • The proxy image changes (the error routes, the encoder skipping HEAD, the widened dot-segment rule in the entrypoint). The proxy is in the stop-gated tier: a plain tale deploy leaves a running proxy untouched and names it in a hint, so pass --stop to take the new edge rules (a brief downtime while db, object-store and proxy recreate); until then the edge keeps 0.5.23's rules while the platform's own changes apply. An own-Compose deployment pulls the new tale-proxy tag.
  • A managed bundle now carries cli/tale.mjs, the interpreted CLI, beside cli/tale. deploy prepare needs that file beside the executable it runs as, which the setup-cli action and bun run --filter @tale/cli build produce at this commit; the executable from the release page has no interpreted bundle beside it and cannot prepare a managed bundle — build the CLI from the pinned commit, as the CLI install page describes.
  • The upload allowlist keys on the file name alone from now on; a client that relied on a declared MIME type for an extension-less name must name the file with its extension. Files already stored are untouched.
  • No configuration-file change and no new environment variable; .env.example is untouched. The db, web, sandbox, sandbox-runtime, sandbox-buildkitd, sandbox-egress and sandbox-llm-gateway images have no source change in this range; the docs image carries the updated pages in all three languages.
  • Operators of the hosted platform: the IPv6 item above is a Docker daemon setting on the host (ip6tables, no userland proxy, an IPv6-enabled proxy network), not a platform release.

Upgrading

  • On the 0.5 line (0.5.0 – 0.5.23):

    tale update
    tale deploy --stop

    --stop recreates db, object-store and proxy so the new edge rules take effect (a brief downtime); a plain tale deploy applies everything else and leaves the running proxy on 0.5.23's rules. The migrations above apply on the first boot of the new images.

  • Managed deployments move by pinning the CLI and the runtime to this release's commit, preparing a new bundle and applying it with the pinned CLI — see Managed deployments on the CLI install page. Pin the CLI at this commit, not an older one: the bundle's backend-local phases run under the interpreted CLI that #3333 ships. On a Linux x64 host whose CPU lacks AVX2, pass linux-baseline: 'true' to the setup-cli action so the bundle embeds the baseline executable.

  • New install:

    curl -fsSL https://raw.githubusercontent.com/tale-project/tale/main/scripts/install-cli.sh | bash
    mkdir tale-05 && cd tale-05
    tale init
    tale deploy

    On a CPU without AVX2 the downloaded executable aborts with Illegal instruction; build it from source with bun run build:linux-baseline in tools/cli instead.

What's Changed

  • fix(cli): repair on-prem deploy provision and credential path by @yannickmonney in #3333
  • fix(cli): run backend-local client export under interpreted bun by @yannickmonney in #3336
  • fix(platform): close the 2026-09-13 API evaluation's fifth-pass findings by @larryro in #3335

Full Changelog: v0.5.23...v0.5.24