Skip to content

Tale v0.5.26

Choose a tag to compare

@larryro larryro released this 14 Sep 12:49
f4ebf4d

0.5.26 is a patch tag on the 0.5 line that carries more than its fixes — read it as a minor. It ships two forward-only schema migrations, three optional environment variables, an API contract that moves from 1.6.0 to 1.9.0 in three additive steps, a new tale-ui-docs image and CLI changes for managed deployments; nothing in it is breaking, no configuration file changes shape, and the upgrade is still tale update followed by tale deploy --stop (the proxy image changes — see Upgrading). Fourteen changes since 0.5.25: the seventh external API evaluation's findings closed at their root, the platform's reusable components folded into @tale/ui with a design-system site of their own, a navigation memory and tabbed automation pages in the app, a notification export for connected applications, a hostname migration and a container-name prefix for managed deployments, and aggregate analytics that stays off unless an operator switches it on.

Highlights

The seventh API evaluation pass (#3353)

The seventh external black-box evaluation of the REST and MCP API ran against 0.5.25 and reported 3 S2, 24 S3, 34 S4 and 9 lead-lane findings. Every finding was second-confirmed against the source; all three S2 are real defects, fixed at the root with regression tests and integration lanes, and the rest split three ways: code defects fixed, contract-precision items closed in the OpenAPI document and the en/de/fr reference, and thirteen product gaps recorded in the contract debt ledger with their pay-down (listed under Known issues).

  • A mirrored conversation could re-home onto a recreated contact. DELETE /api/v1/contacts/{id} frees the contact's externalId, and the next POST /api/v1/conversations/sync re-resolved the conversation's externalContactId onto whichever contact now carried it, so an unrelated contact inherited the history. A content snapshot for a conversation whose contact is in the trash answers 409 CONVERSATION_CONTACT_TRASHED and applies nothing; a deleted: true teardown still closes the mirror; the receipt reports contactStatus (active, trashed, missing) and the bound externalContactId; an existing conversation's contact is never re-resolved.
  • Documents released from a deleted project stayed unfindable. A project delete in detach mode (and the app's delete) released the documents without re-stamping the corpus rows retrieval pre-filters on (project_id, team_ids, folder_path), and a retry-indexing reported success while changing nothing. Both delete doors re-stamp the released documents after the commit, and the indexer's skip-unchanged branch re-stamps off the current scope, so a retry heals a drifted stamp.
  • Six concurrent task starts began five runs. The start guard was a check-then-act inside a SERIALIZABLE transaction whose snapshot froze before the advisory lock, so every racer read "no live run". The rule now lives in the schema: migration 0102 collapses existing live duplicates onto the earliest and adds the partial unique index automation_runs_one_live_per_task; the REST door starts under READ COMMITTED, and a loser's unique violation is reconciled by re-reading the winner (already_running, never a second billable run). The integration lane starts six at once and counts one run.
  • Runs say why they failed. Run.failureCode and RunSummary.failureCode carry one enum over the engine's, the provider's and the agent turn's causes — node_error, connector_error, credit_exhausted, rate_limited, context_length, turn_crashed, deadline, ask_expired and twenty more — written when a run lands on failed (migration 0103); a cancel clears the stale park detail and answers the run's status beside cancelled; GET /api/v1/me answers capabilities.developer.
  • Indexing names its cause and knows what is terminal. indexing.errorCode is a closed vocabulary of thirteen values: unsupported_type, image_no_vision, empty, not_text and malformed are terminal (unsupported — a retry reproduces the answer and is refused as terminal), the rest read failed with the job retrying (embedding_upstream, indexer_error, index_rebuilding, …). NUL bytes are stripped before the PII policy and the chunker (a .txt that was really binary used to fail after the embedding was paid for); the stored mimeType is resolved from the extension first (json, yaml, yml mapped); GET /api/v1/projects/{id}/files/{documentId} reads one file row with its indexing state — the single-file read 0.5.24 and 0.5.25 listed as missing.
  • The crawler explains an empty page. A page the crawler looked at and stored nothing for is unsupported_content instead of a row that reads unfetched; X-Robots-Tag: noindex is honoured (robots_noindex); a TLS handshake failure is tls_error; a same-host link naming a port is never dialed; a scan that stored nothing ends on error with its reason instead of active; Website.status is an enum and ?status= refuses a value outside it.
  • Contract doors hold their line. A blank or over-long Idempotency-Key answers 400 INVALID_HEADER (a blank header no longer reads as "no key"); POST /api/v1/contacts/bulk judges rows one by one; contacts?source= is the closed set; products.imageUrl is held to the crawl-target host rule as a string, never fetched; a contact, product or project PATCH that changes nothing writes nothing; If-Match on the document update answers 412 PRECONDITION_FAILED with data.etag; the REST 429 carries a sentence and a requestId like every other refusal; a body string with a NUL or a lone surrogate is refused with its path.
  • Chat, MCP and the edge. A reply that settles with no text carries no empty text part (a reasoning-only cut keeps its reasoning part); a tool call the reply cap withheld says whether its arguments were cut or complete; every MCP string argument refuses a blank — whitespace alone included — at the transport (JSON-RPC -32602); a malformed HTTP/1.1 chunked request body answers 400 BODY_CHUNK_MALFORMED at the edge instead of the 502 outage envelope with retry advice; //api/v1/… answers the JSON 404; robots.txt no longer pairs Allow: / with Disallow: / and carves out the developer pages; bluetooth left the Permissions-Policy, and with it the one console warning every page load carried.
  • A cloud-drive folder sync files its documents. A OneDrive or Google Drive sync import whose files the hub already knew — an earlier one-time import at the root, a directly picked file — left them at the root with no folder row and nothing to stop the sync from. The sync root now exists from the moment the sync configuration does, and an adopted, unchanged document is moved into the folder its selection names. A live organization heals on its next scheduled sync.

One design system, two packages, and a site to read them (#3351)

  • @tale/ui holds every reusable platform component. 324 files moved out of the platform app into packages/ui (152 new export subpaths), their message keys into the package catalog, tests and stories alongside; the platform keeps only the wrappers that carry business logic — org-branded logos, the ability-gated tab strip, the app sidebar, error-scope and accent-colour providers — and imports everything else through @tale/ui/<subpath>.
  • @tale/marketing-ui is the marketing language, split out of @tale/ui and the web site — site chrome, primitives, feature frames, product-demo frames, entrance motion, marketing tokens — layered on @tale/ui; routing stays the host's.
  • Both packages install from GitHub with Bun. Bun cannot install a package from a subdirectory of a git repository, so every push to main republishes each package as a root-level snapshot on the dist/ui and dist/marketing-ui branches, and every release pins the same snapshot with a ui-v<version> and a marketing-ui-v<version> tag: "@tale/ui": "github:tale-project/tale#ui-v0.5.26" is reproducible, the branch moves. The consumer contract is in packages/ui/README.md.
  • docs.tale.dev wears the app language: a navigation rail, one sticky strip with the breadcrumb trail and the page actions, a mobile header with a drawer, neighbour cards and a compact footer — no marketing chrome. Three defects went with it: ancestor rows claiming aria-current, a drawer that needed two Escapes to close, an invisible scrim after a viewport change.
  • ui.tale.dev is a new service, services/ui-docs (port 3003, image tale-ui-docs, its own compose file): the front page in the marketing language, every /docs/* page in the app language, markdown content with live examples backed by the real components, search, prerender and the SEO artifact set, and a container test in the release gate. Nineteen pages seed it; the contributor-setup page points at it.

The app remembers where you were (#3347, #3345, #3338, #3346, #3340)

  • Each rail section reopens the place you last had open there — the task board you were on, not the projects list — and clicking the section you are already in goes to its default entry, the one-click way out of a deep page. The memory is per tab (two tabs on different projects do not overwrite each other), seeded across restarts, and expires eight hours after your last navigation; a deleted project or automation forgets its place. Section roots do not redirect, so a shared /projects link still means what it says. The Knowledge rail gained its missing knowledge-entries item, and a deleted project's page carries a link back to the list.
  • Automations are built like projects. The list ends its title row in a divider above the toolbar and its button reads Create automation; an automation's page has three tabs — Editor, Versions, Runs — with the version picker, Deploy this version, the run verbs and Save/Discard at the right end of the tab strip; the bare URL forwards to /editor, the canvas version lives in ?version=, an unknown slug renders the not-found state instead of loading forever, and below md the verbs sit in the floating dock. The switcher in the breadcrumb lists every automation in the organization even from inside a project — organization-level entries above a divider, project-specific ones below — keeps the tab you were on, and resets the editor's version, draft and inspector when the automation changes.
  • A notification link survives login. Opening a protected task, document or conversation link in a signed-out browser used to land on a bare login page; both dashboard guards now carry the full destination through the login return, and expired-session recovery keeps query values and fragments too.
  • Loading placeholders match what they replace. Skeletons mask the real controls and reuse the loaded layouts for forms, tables, cards, chat, tasks, project tabs and document previews — no jump when the data lands — and a quick route transition no longer flashes the top progress bar; the masks follow the theme and the reduced-motion preference.

A notification export for connected applications

GET /api/v1/notifications/sync lets an organization owner or administrator mirror every notification visible to a verified member: the personal and the organization stream, walked independently (recipientEmail and stream are required; cursor, limit 1..100 and locale optional) with signed cursors scoped to the recipient, the stream and the organization. Each row carries a stable, org-prefixed id, a version that moves with content or read state, title, body, read, createdAt and the path the bell itself would open — agent-question task and run links included — relative to the deployment's browser origin, in the requested locale or the organization's default. The export never marks a notification read and never deletes one; a missing, disabled, unverified or ambiguous recipient answers an empty completed page, and a 304 answers an unchanged ETag. Contract 1.8.0.

REST task intake binds the Setup folder by name (#3339)

Folder-driven automations — the VAT return desk among them — read a task's externalUrl as the id of the project's Setup folder, and the app's intake resolves that folder by name; the REST door refines externalUrl to an absolute URL and had no way to bind a folder, so an external desk on an API key could not use them at all. POST /api/v1/projects/{id}/tasks takes an optional setupFolderName: the project's root folder of that name (matched without regard to case) is resolved inside the intake transaction — on the create and again on every repeat — and stored as the task's externalUrl; a name no root folder carries answers 400 SETUP_FOLDER_MISSING and creates nothing, and sent beside externalUrl it is refused as INVALID_BODY. Contract 1.7.0.

A managed deployment can change its hostname, and name its containers (#3343, #3344, f4ebf4d)

Changing a managed Tale deployment's origin used to fail against the retained bootstrap, email-attestation and OAuth client journals. identity.migrateOriginFrom: "https://old.example.org" in the deployment declaration binds the exact previous HTTPS origin: the CLI re-authenticates the retained account, verifies the existing client secret and updates only the journals' origin bindings — identity ids and secrets stay — and the native configuration receipt migrates with it, keeping the organization id and slug and verifying every resource through the normal plan and readback flow. Missing, pending or unrelated journals refuse; an interrupted migration resumes with a mixture of completed old- and new-origin journals; a configuration write interrupted at the new origin resumes its exact pending plan, and a pending receipt at the old origin blocks the migration. After the ready receipt, drop the declaration and export consumer configuration for the new issuer; reversing is the same flow with the origins swapped. The CLI install page and the CLI README carry the walk-through.

A managed deployment can also name its containers so an environment is recognisable in a container listing: optional runtime.containerPrefix — a lowercase hyphenated slug of at most 40 characters, north-desk-prod say — gives every managed service the container name <prefix>-<service> (north-desk-prod-db, north-desk-prod-backend-api) while the deployment's identity, Compose project, state paths and named volumes stay what they were; service DNS names do not change. Adding, changing or removing the prefix recreates the managed containers, which can briefly interrupt service, and readiness requires the observed container names; a rename that is interrupted replays, an unchanged one is a no-op, and a naming conflict refuses. It stays one complete managed runtime per Docker daemon — the prefix allocates no separate ports, sandbox networks or host workspaces.

Aggregate analytics, off unless you switch it on

The platform, the web site and the docs site can report aggregate traffic to a self-hosted Umami collector. It is an opt-in per deployment: set UMAMI_URL, UMAMI_WEBSITE_ID and UMAMI_PROXY_TOKEN together — all three, read at runtime, so clearing the website id switches it off without a rebuild; unset, nothing is loaded and nothing is sent. A first-party proxy serves the tracker and forwards only pageviews — and, on the marketing site, completed contact and demo submissions without their contents — with the token held server-side; a private platform route is reported as its template with placeholders for organization and resource ids; no cookies, no persistent identifiers, no page titles, query strings, form contents or session replay; Do Not Track and Global Privacy Control disable it. The edge sets X-Analytics-Client-IP from the client address it trusts on the platform and docs upstreams, so a browser-supplied value never counts. The observability page has the rollout check; the privacy policy on tale.dev and the docs' legal page describe what is collected.

Self-hosted providers on private addresses reach the sandbox gateway

A provider on a private address could not start a sandbox agent turn even with TALE_ALLOW_PRIVATE_PROVIDER_HOSTS=1 set: the LLM gateway rejected the upstream URL. The platform now sends the gateway's allow_private_network setting for a private host that opt-in admits; a custom provider URL passes the host policy before any gateway I/O and before a cached provision is reused; cloud metadata endpoints stay refused (the AWS IPv6 endpoint included), and a public provider keeps the gateway's default safeguards.

Behaviour changes

  • Clicking a rail section opens the place you last had open there (an eight-hour memory, per tab); clicking the section you are in goes to its default entry. /projects and the other section roots still render their lists when opened by URL.
  • Automations: the list's button reads Create automation (was New automation); an automation opens on …/editor under the Editor | Versions | Runs tab strip; a Versions row opens the editor at that version (?version=); a run page opens under the same strip with Runs lit; an unknown automation slug renders Automation not found. The breadcrumb switcher lists every organization automation from any project, in two groups.
  • A notification link opened signed-out lands on its task, document or conversation after login instead of on the dashboard; the destination keeps its query and fragment.
  • Loading skeletons take the shape of the loaded page; a quick navigation no longer flashes the progress bar.
  • A OneDrive or Google Drive sync creates its root folder with the sync configuration and moves adopted, unchanged documents into the selected folder on the next scheduled sync.
  • A run that fails on this build answers failureCode beside its sentence; cancelling a parked run clears the park detail and the cancel answers the run's status.
  • Six simultaneous starts of the same task's automation produce one run; any live duplicates the old race left behind are cancelled by migration 0102 at boot (the earliest run of each group is kept).
  • Deleting a contact and recreating it with the same externalId no longer hands the old contact's mirrored conversations to the new one: a snapshot for the trashed contact's conversation is refused with 409 until the contact is restored or the mirror is torn down.
  • Documents a deleted project released (detach mode) are findable in the hub again; retry-indexing on such a document re-stamps its scope.
  • Knowledge indexing: a document that cannot be indexed reads unsupported with a terminal errorCode, and a retry on it is refused as terminal; a text file that is really binary no longer costs an embedding before it fails; .json, .yaml and .yml are typed from the extension.
  • Website crawling: a page that stored nothing reads unsupported_content, a page answering X-Robots-Tag: noindex reads robots_noindex, a certificate failure tls_error; a same-host link naming a port is not dialed; a scan that stored nothing ends the site on Error with the reason (was Active).
  • A REST PATCH on a contact, product or project that changes nothing writes nothing: updatedAt stays, no audit row, no event — a retry is free.
  • The REST 429 body carries a sentence in error and a requestId (error used to repeat the code); the in-app doors are unchanged.
  • An Idempotency-Key header sent blank or over 255 characters is refused with 400 INVALID_HEADER and nothing starts (a blank used to read as no key).
  • MCP: a blank string argument — whitespace alone included — on any tool is refused at the transport as JSON-RPC -32602 and no tool runs (0.5.25 refused the empty string on three arguments).
  • A chat reply that settles with no text carries no empty text part; a tool call the reply cap withheld says whether its arguments were cut or complete.
  • The edge answers 400 BODY_CHUNK_MALFORMED to a malformed HTTP/1.1 chunked request body (was 502 UPSTREAM_UNAVAILABLE with retry advice) once the proxy is recreated (see Upgrading); //api/v1/… answers the JSON 404.
  • robots.txt carves out the developer pages instead of pairing Allow: / with Disallow: /; the /docs page links the developer guides and /openapi.json; the user menu's Documentation item opens the docs site; the app shell's <noscript> names both.
  • Permissions-Policy no longer lists bluetooth on the platform, web and docs responses; the "Unrecognized feature: 'bluetooth'" console warning on every page load is gone.
  • docs.tale.dev reads in the app language (rail, sticky strip, a drawer on phones); the ancestor-row aria-current, the two-Escape drawer and the invisible scrim are fixed.
  • With the three UMAMI_* values set, the platform, web and docs sites load a first-party tracker and report pageviews; without them nothing changes.
  • A self-hosted provider on a private host, admitted by TALE_ALLOW_PRIVATE_PROVIDER_HOSTS=1, starts sandbox agent turns.
  • tale deploy on a managed deployment honours identity.migrateOriginFrom and runtime.containerPrefix; adding, changing or removing the prefix recreates the managed containers under their new names.

API contract changes

The OpenAPI document moves from 1.6.0 to 1.9.0 in three additive steps, each dated 2026-09-14; the Error.code enum grows from 145 to 149 values (BODY_CHUNK_MALFORMED, CONVERSATION_CONTACT_TRASHED, PRECONDITION_FAILED, SETUP_FOLDER_MISSING). Nothing was removed or renamed.

Added

  • 1.7.0 — POST /projects/{id}/tasks takes setupFolderName (optional; sent beside externalUrl it answers 400 INVALID_BODY); 400 SETUP_FOLDER_MISSING when no root folder of the project carries the name.
  • 1.8.0 — GET /notifications/sync (recipientEmail and stream required; cursor, limit 1..100, locale): a keyset page (page, isDone, continueCursor, recipientId) of one member's personal or organization notifications, each with a stable id, a version, title, body, path, read and createdAt; owner or administrator only; 200, 304 on an unchanged ETag.
  • 1.9.0 — GET /projects/{id}/files/{documentId}: one project file row with its indexing state, an ETag and 304; Run.failureCode and RunSummary.failureCode (present with status: "failed" on a run that failed on this build or later); Me.capabilities.developer; externalContactId and contactStatus on the conversation-sync receipt; 409 CONVERSATION_CONTACT_TRASHED on a content snapshot for a trashed contact; If-Match on PATCH /documents/{id} → 412 PRECONDITION_FAILED with data.etag; WebsitePage.lastErrorKind gains unsupported_content, robots_noindex and tls_error; the edge's own 400 BODY_CHUNK_MALFORMED.

Changed

  • 429 — error is a sentence naming the wait and the envelope carries requestId (was error: "RATE_LIMITED"); code, Retry-After and data.retryAfterMs are unchanged.
  • Idempotency-Key — blank, or over 255 characters, answers 400 INVALID_HEADER and nothing starts (a blank was read as no key).
  • POST /contacts/bulk — rows are created independently: a row the schema refuses fails alone, under errors[] as INVALID_BODY with its field-named issues, while every valid row lands (201); only the batch's own shape refuses the whole call.
  • GET /contacts?source= — the closed set; a value outside it answers 400 INVALID_QUERY.
  • Website.status — the enum idle, scanning, active, error, deleting; GET /websites?status= refuses a value outside it with 400 INVALID_QUERY.
  • Product.imageUrl — held to the crawl-target host rule as a string, never fetched: a loopback, link-local, private-network or cloud-metadata host answers 400 INVALID_BODY (private hosts admitted by TALE_ALLOW_PRIVATE_CRAWL_HOSTS, metadata hosts never).
  • PATCH /contacts/{id}, PATCH /products/{id}, PATCH /projects/{id} — a body that changes nothing writes nothing and answers the current representation; updatedAt does not move.
  • Automation.document.version — the schema declares the integer the wire always carried.
  • indexing.errorCode — the closed vocabulary of thirteen values, present with failed and unsupported; retry-indexing on an unsupported document answers {"status": "skipped", "reason": "unsupported"}.
  • POST /runs/{runId}/cancel and POST /projects/{id}/runs/{runId}/cancel — answer the run's status beside cancelled, and a cancel clears a parked run's detail.
  • The MCP endpoint — every string argument on every tool refuses a blank, whitespace alone included, as JSON-RPC -32602 at the transport (0.5.25 held name, runId and query to minLength: 1).
  • Chat Message parts — a reply that settled with no text carries no empty text part; a tool-call part the reply cap withheld says whether its arguments were cut or complete.

Documented, unchanged on the wire (en, de, fr): webhook delivery dedupe is by the delivery id, never the body, with the header precedence list on the parameter and the 202; automation authoring lives on the MCP endpoint, in the app and in tale deploy (the Automations tag and the reference row say so); accepted thread sends form one oldest-first queue shared by the whole deployment, worked in batches of up to five turns (the operator's WORKER_CONCURRENCY), so a burst completes in waves; /health on a deployment's origin is the proxy's own liveness and a path no route owns answers the app shell with 200 — monitor /status.json or /api/health; the crawler's ceilings (10,000 tracked URLs per site, a scan of at most 200 five-minute links, 25 MB and 30 seconds per page, a discovered page dropped after five failed scans) and that it honours robots.txt Disallow for the * agent while discovering, never for a URL you listed; website search is BM25 with score semantics; what startedAt means on a run; Contact.email and Product.imageUrl carry their formats; the compression floor sits in the document's description; the keyless jq recipe over the Error.code enum; the tutorial's key placeholder reads <api-key>.

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 not exercised on a running deployment. The chunked-body rule (BODY_CHUNK_MALFORMED) and the X-Analytics-Client-IP header this release adds to the edge went into a tale-proxy image no deployment has run yet, and services/proxy has no test of its own; the manual register carries the printf recipe that provokes the refusal. The 0.5.25 gap (the one-year HSTS value on the edge's own refusals) is closed: the hosted platform's edge has answered max-age=31536000 on its own 404 since 2026-09-14.
  • The rail's navigation memory awaits its manual round: restore, re-entry, expiry, a deleted target and two-tab isolation are manual boxes (NAV-F16–NAV-F19, NAV-B6–NAV-B9); the automated suites cover the store and the rail's resolution, not the browser choreography.
  • The docs screenshots automations-catalog and automation-editor-canvas still show the previous automations layout; the text beside them is current.
  • ui.tale.dev is not live at this tag. tale-ui-docs:0.5.26 exists from this release, but the site goes live with the operator's deployment of it; until then the link on the contributor-setup page does not resolve. Nineteen pages seed the site — one page per remaining component family is follow-up work — and the docs chrome is duplicated between the docs site and the design-system site rather than shared.
  • A reply-language directive is a directive: a model may still answer in the prompt's language and nothing on the wire marks a slip.
  • No image input on the REST chat send. A vision model reads an image over REST only on a thread the app continued with an image attachment; the design of an attachments field on the send is recorded as contract debt.
  • No REST door authors or deploys an automation — POST /automations answers 405 by design. Build and deploy in the app, or over the MCP endpoint's save_automation and deploy_automation; the REST key lists, reads, runs and wires triggers.
  • 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 part now says its arguments were cut, but the raw text the model emitted is still not on the transcript.
  • Folder names written before 0.5.24 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.
  • Recorded as contract debt by the seventh evaluation, each with its design in the ledger: a run carries no usage or cost; approvals and asks have no REST twins (a run parked on waitingFor: approval or ask is decided in the app); a task cannot be archived or deleted over REST; a webhook bind does not say whether the deployed inputs schema admits a delivery; an exhausted repeatUntil is only a trace note; Website carries no scanStartedAt, <meta name="robots" content="noindex"> is not honoured (the header is) and the crawler has no page cap, path filter or stop verb of the caller's; website search has no dense leg and its substring fallback (a knowledge database without ParadeDB) stamps score: 0 silently; no Idempotency-Key on the task start, so a retry after the run finished starts another; no queue position on a queued send; a corrupt Office document (docx, pptx, xlsx, odt) still fails as indexer_error and is retried five times where a PDF lands malformed; no /.well-known/security.txt; no changelog feed on tale.dev; no SDK, collection or per-code table beyond the Error.code enum.

Migration notes

  • Two platform migrations, both forward-only and rolling-safe, applied at boot in filename order inside the advisory lock while the previous image keeps serving:
    • 0102 automation_runs_one_live_per_task first collapses any duplicate live runs (queued, running, waiting) that share an organization, an automation, a project and a task subject onto the earliest-started run, marking the rest cancelled — the race's own residue; a correct platform never produced them — then adds a partial unique index over that key. The previous image cannot violate it: it only ever starts the runs it already started.
    • 0103 automation_runs.failure_code adds a nullable text column with no backfill: a run that failed before this release keeps its sentence and answers no failureCode (read the absence as "unknown"). The previous image neither reads nor writes it.
    • No knowledge-database migration. 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.
  • Three new optional environment variables — UMAMI_URL, UMAMI_WEBSITE_ID, UMAMI_PROXY_TOKEN — in the root .env.example (the platform) and in the web and docs sites' own. All three unset means analytics off, which is the default and the previous behaviour; no configuration file changes shape. A managed deployment carries them through its runtime environment when they are declared.
  • The proxy image changes — the chunked-body 400 and the X-Analytics-Client-IP header it sets on the platform and docs upstreams. 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 rules (a brief downtime while db, object-store and proxy recreate); until then a malformed chunked body still gets 0.5.25's 502 envelope. An own-Compose deployment pulls the new tale-proxy tag.
  • A new image, tale-ui-docs, joins the release matrix — eleven images, amd64 and arm64 — with its own compose file (compose.ui-docs.yml) and container test. It is not part of the platform stack; nothing pulls it unless you deploy the design-system site.
  • The platform (the backend, the app, and the message catalogs the notification export reads), proxy, web (the marketing-language package, the analytics opt-in, the privacy policy) and docs (the app-language chrome, the updated pages in en, de and fr) images carry source changes; the db, sandbox, sandbox-runtime, sandbox-buildkitd, sandbox-egress and sandbox-llm-gateway images have none. The CLI has source changes — identity.migrateOriginFrom, runtime.containerPrefix and the analytics pass-through — and the release executables report 0.5.26.
  • @tale/ui and @tale/marketing-ui are pinned by this release as the ui-v0.5.26 and marketing-ui-v0.5.26 tags on their snapshot branches; a consumer outside the monorepo installs "@tale/ui": "github:tale-project/tale#ui-v0.5.26".

Upgrading

  • On the 0.5 line (0.5.0 – 0.5.25):

    tale update
    tale deploy --stop

    The two migrations run at boot. --stop recreates db, object-store and proxy so the edge carries the new rules (a brief downtime); a plain tale deploy applies everything else and leaves the running proxy on 0.5.25's. To switch analytics on, set the three UMAMI_* values in .env before deploying; leave them unset to keep the previous behaviour.

  • 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. The bundle's backend-local phases run under the interpreted CLI (cli/tale.mjs) that the setup-cli action and bun run --filter @tale/cli build produce beside the executable; the executable from the release page has no interpreted bundle beside it and cannot prepare a managed bundle. A hostname change is a deployment with identity.migrateOriginFrom declared, as above. 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

Full Changelog: v0.5.25...v0.5.26