Skip to content

Tale v0.5.39

Choose a tag to compare

@larryro larryro released this 20 Sep 03:42
b14b05b

0.5.39 carries 1 merged pull request. It closes every finding of the 2026-09-19 external evaluation of the machine surface (the eleventh round, run against 0.5.36 and contract 1.17.0), the first round that found nothing above the third severity: seven S3 and twenty-seven S4, each re-confirmed in code before it was touched — one, a pagination marker the evaluator read without resolving a reference, needed nothing. The largest fixes are the ones a client feels: an organization's teams can be listed over REST at last, so an integration can set a project's audience without a person copying an id out of the app; archiving or deleting a chat can no longer race a message that was accepted a few milliseconds earlier; the OpenAPI document no longer carries keywords beside a $ref, which every generator silently dropped and typed three documented nulls as non-null; the audience and agent doors follow the no-op rule every other write already follows; the MCP endpoint answers the citation fields, the exact-integer refusal and the run history that REST already answered; a knowledge entry written over REST says so; a question a cancelled or expired run stopped taking is retracted on the task's timeline; and the crawler refuses a bare IP address and reads a site's robots.txt once, not twice, when a site is registered. The contract moves from 1.18.0 to 1.19.0 with one new read, one new schema and no new error code. One migration (0111) widens a check constraint, no environment variable changes, and no image in the stop-gated tier changes, so the upgrade is tale update followed by a plain tale deploy.

Highlights

Teams are readable over REST (#3428)

A project's teamIds, a Hub document's teamIds and a skill's teams take team ids, and nothing on the machine surface answered one: GET /api/v1/me carries no teams, no list existed, and the reference never mentioned teamIds, so a caller could set an audience only with an id a person had copied out of the app. GET /api/v1/teams answers every team of the organization — id, name and member, whether the key holder belongs to it — by name, as a complete set (x-tale-pagination: none), for any key holder: a team's name is what every audience badge shows, so the list is the app's own directory read, not the holder's memberships. member is the pre-flight for TEAM_ACCESS_DENIED: an organization admin may assign any team, another role only the teams it is a member of. The surface stays read-only by design — teams are created, renamed and staffed in the app (Settings > Teams) or by an identity provider, and no operation on this surface writes one. The reference gains a Teams row and a paragraph under Find or create the project that walks the audience end to end.

Archiving or deleting a chat can no longer race a send (#3428)

The REST archive and delete doors checked for a running turn and then wrote; a message accepted in the ten to thirty milliseconds between the check and the write landed on a thread that was gone — the turn's job opened on a trashed thread and vanished, and a client polling the send saw 404 forever — or on an archived one, where the worker settled it as cancelled. The fence is now the write's own predicate on the thread row: the archive and the trash UPDATE carry status = 'active', no queued send (generation_queued_since_ms IS NULL), no generating turn (generation_status IS DISTINCT FROM 'generating') and no open generation, on the columns the send's claim and the worker's open write in their own transactions, so the row lock serialises the two and a send that committed first is seen. A door whose write was held back re-reads the row in a fresh statement and answers what it finds: 409 CHAT_TURN_IN_PROGRESS when a turn claimed it, and the idempotent answer when another writer already archived or trashed the thread. The send's claim gained the mirror-image predicate — it lands only on the caller's active, un-archived thread — and, when it loses, answers 404 THREAD_NOT_FOUND or 409 CHAT_THREAD_ARCHIVED from the same fresh read instead of opening a turn on a thread that is gone. The app's own archive keeps its unfenced behaviour (a turn that lands in an archived thread is settled by the worker); the app's trash answers the same ok: false it always did, now from the predicate rather than a separate read.

The OpenAPI document is generator-clean again (#3428)

The three read doors 1.16.0 added — GET …/runs/{runId}/ask in both scopes and GET …/tasks/{taskId}/review — declared their idle null as {"$ref": …, "nullable": true}, and PendingAsk.questions carried a description beside its $ref. A Reference Object cannot be extended (OAS 3.0.3 §4.7.23): every reader ignores the sibling, so every generated client typed a documented null as non-null. Those were the only four $ref-with-sibling sites in the document; the builder's nullable() now wraps a reference in allOf and carries nullable beside it, and a typeless oneOf (ContactInput and ContactPatch.externalId) carries nullable on each branch, where a nullable on the bare oneOf compiles nowhere. Three guards in the spec test hold the document to it: no Reference Object has a sibling keyword, every nullable sits beside a type or an allOf-wrapped reference, and the ask and review doors declare their idle answer as a nullable reference.

The audience and agent doors follow the no-op rule (#3428)

Re-asserting the audience a project already carries bumped updatedAt on every pass — a sync that re-asserts on every run moved the stamp every time and invalidated every other client's expectedUpdatedAt — where a name no-op did not; a repeated team id was a 400 on projects while documents and skills collapsed it silently; an archived project, frozen to every other write, still took a teamIds change; and a byte-identical agent PUT bumped updatedAt, so two declarative writers re-asserting one configuration 409'd each other with PROJECT_AGENT_STALE. Each now follows the rule the document doors set: the audience the project already carries is a no-op (no write, no audit row, updatedAt kept — order counts, because teamIds[0] is the mirrored owning team); a repeated id collapses to one, first-seen order kept, and the stored list is what the response echoes, while a blank entry is still PROJECT_SHARING_INVALID; PATCH {teamIds} on an archived project is 403 PROJECT_ARCHIVED unless the same body restores it — the gate now sits above the sharing call, so nothing is written first; and an agent PUT whose body names the configuration already stored writes nothing. The 400 text on both project doors now names INVALID_BODY for a teamIds past its maxItems, the code the wire always answered.

The MCP endpoint answers what REST answers (#3428)

Four gaps between the two doors are closed. get_knowledge passages carry documentId, corpus, chunkIndex and projectId — the citation the REST search answers, so an MCP client follows a hit to its document without a second search over REST. A JSON-RPC body carrying a whole number beyond ±(2^53 − 1) is refused as an invalid request (-32600) naming the literal's path, the way the REST door names it under INVALID_BODY, instead of being rounded and echoed — a client keying its correlation table on 64-bit ids never matched the reply; the two doors share one parser now, lib/utils/json-exact.ts. list_runs {name} answers a deleted automation's kept runs, and so does REST GET …/automations/{name}/runs (the project twin included), where both used to say the automation never existed — a run's history outlives its automation by design, and only a name neither an automation nor a run ever bore is AUTOMATION_NOT_FOUND. get_catalog narrowed to a core node kind (transform, llm, agent, subautomation) answers the hint search_catalog already gave — get_docs describes it — instead of an empty list, and start_run on a version that is not the deployed one names run_automation {mode: "mock"} rather than a "mode" the tool does not have. The reference now says the Idempotency-Key HTTP header is refused on the endpoint (400 INVALID_HEADER, the wire's long-standing answer), not "not read": a batch carries up to 20 calls, so the key rides in the tool arguments.

A knowledge entry says where it came from (#3428)

app.knowledge_entries.source names the lane a fact arrived through — chat for the assistant's capture, manual for the form — and the REST door stamped manual too, so an operator reading the table's Source column could not tell an integration's import from a hand-typed fact. Migration 0111 widens the check constraint to admit api; a create or a supersede over POST/PATCH /api/v1/knowledge-entries writes it, KnowledgeEntry.source is the enum chat | manual | api, and the table and the entry dialog show an API badge in English, German and French. Rows the door wrote before this release keep manual, honestly the value they carried. Beside it, a PATCH of a superseded row used to name its direct successor, which may itself be superseded, sending a client down the chain one 409 at a time; the 409 now names the topic's active row in data.activeId (and the successor in data.supersededBy), so a stale id costs one round trip. The fusedScore description says what the divisor actually is — the best score possible given the legs that contributed candidates to this response — and products are documented as what they are: deleted outright, no trash and no restore, the name and externalId free for a new product at once.

A question that stopped taking an answer is retracted (#3428)

An ask_human node posts a "Question for you" card on the task timeline; when the run was cancelled or the question expired, the card stayed the newest comment, inviting a person to answer a question the door then refused with HUMAN_ASK_NOT_PENDING. The closure now posts a retraction as the same trusted workflow actor, in the same automated voice, on the same task — "the question above no longer takes an answer", with the reason: the run was cancelled, the run ended, or the question expired before it was answered. It is best-effort like the card itself; a task retired in the meantime has no timeline to retract on. On the answering door, a blank answer — whitespace or invisible format characters only — is now the documented 400 EMPTY_ANSWER: the code has been in the registry since 1.16.0, but the schema's own minimum-length check spoke first, so no client ever saw it.

The crawler registers a site politely, and only by hostname (#3428)

A bare public IP address registered as a website (201) and then scanned straight into a TLS error: the crawler dials by host name and verifies the certificate against it, which no IP literal can present. It is now 400 WEBSITE_DOMAIN_INVALID, judged after the host policy so a loopback, private or metadata address keeps its clearer WEBSITE_DOMAIN_NOT_CRAWLABLE. Registration itself was impolite: the homepage probe and the first scan started together and read robots.txt twice within milliseconds, and the probe's homepage read went out unpaced. The probe now runs before the first scan is queued, persists its verdict on the corpus row — the sitemaps it advertised included, and a 4xx, which is an answer (the site publishes no rules) — and the scan reuses a verdict younger than a minute instead of dialing /robots.txt again (RFC 9309 lets a crawler cache it for a day); the homepage read waits the site's Crawl-delay after the robots read, and every discovery request — each sitemap and each link-walk read — is paced from the previous request the way a content fetch is, where the first sitemap and the first homepage read used to go out unpaced.

Behaviour changes

  • PATCH …/threads/{id} with archived: true and DELETE …/threads/{id} refuse from the row lock: a turn that claimed the thread between the door's read and its write is 409 CHAT_TURN_IN_PROGRESS; a thread another writer trashed meanwhile is the same 404 or idempotent answer the read would have given. A send that loses the same race answers 404 THREAD_NOT_FOUND or 409 CHAT_THREAD_ARCHIVED instead of 202.
  • GET /api/v1/teams is new: {teams: [{id, name, member}]}, a complete set, any key holder, ETag and 304 like every read.
  • POST …/asks/{askId} answers 400 EMPTY_ANSWER for a blank answer (was INVALID_BODY).
  • GET …/automations/{name}/runs and the project twin answer a deleted automation's kept runs (was 404); MCP list_runs {name} the same.
  • POST /api/v1/projects and PATCH /api/v1/projects/{id}: a repeated team id collapses to one (was 400 PROJECT_SHARING_INVALID); the audience the project already carries is a no-op that keeps updatedAt and writes no audit row; PATCH {teamIds} on an archived project is 403 PROJECT_ARCHIVED unless the same body restores it (was 200 with a real change).
  • PUT …/projects/{id}/agents/{agentId} with the stored configuration writes nothing and keeps updatedAt.
  • POST /api/v1/websites refuses a bare IP address as domain with 400 WEBSITE_DOMAIN_INVALID (was 201 and a tls_error scan).
  • POST and PATCH /api/v1/knowledge-entries write source: "api"; the superseded 409 carries data.activeId and data.supersededBy, and its message names the active row. The Knowledge entries table and dialog show API beside Manual and Chat.
  • PATCH /api/v1/knowledge-entries/{id} on a topic with no active row left says so ("create the topic again") instead of pointing at a superseded successor.
  • MCP: an inexact integer anywhere in the body is -32600 naming its path; get_knowledge passages carry documentId, corpus, chunkIndex and projectId; get_catalog {kind: <core kind>} answers {node_types: [], hint}; start_run on an undeployed version names run_automation {mode: "mock"}.
  • A cancelled, ended or expired ask posts a retraction comment on its task's timeline as the workflow actor.
  • Website registration probes robots.txt and the homepage before the first scan is queued, persists the robots verdict with its sitemaps, and paces the homepage read by the site's Crawl-delay; a scan reuses a verdict younger than 60 seconds; sitemap and link-walk reads are paced from the previous request.
  • The web tier's JSON 404 for /api/* and /.well-known/* paths the API does not serve carries X-Request-Id (a caller's own when it is letters, digits, _ or - up to 255 characters, else a fresh UUID) and Cache-Control: no-store; it carries no X-Tale-Api-Version, because that tier answers no version of the contract.
  • HEAD on /llms.txt, /llms-full.txt, /robots.txt and the app shell carries the GET's Content-Length (was 0).
  • A non-ASCII X-Organization-Slug that names no organization is echoed back as the UTF-8 the caller sent (tälé, not tã¤lã©), still capped and still without a lookup.
  • DELETE /api/v1/browser-sessions/import is 405 with Allow: POST, OPTIONS (it used to reach the pool gate as a delete of a session called import, and OPTIONS advertised DELETE).
  • The chat send's content cap message names its unit — 100,000 UTF-16 code units — where "characters" misstated it for an emoji.
  • Documentation (English, German and French): the Teams row and the audience walkthrough, the 403 bullet's teamIds, run history by name after an automation delete, a project delete taking its run history (unlike an automation delete), a mirrored task starting in backlog, ?version=latest, the guarded skill delete's 404, source: "api" and data.activeId, products without a trash, the onboarding tables' 404 ORG_SLUG_INVALID row, the deploy gate (a version with a failing test cannot be deployed; one with none can), the MCP header refusal, passage fields, list_runs exception and get_catalog hint, the knowledge-entries source badges, and the crawler's hostname rule.

API contract changes

  • 1.18.0 → 1.19.0. 85 → 86 paths, 133 → 134 operations, 62 → 63 schemas; Error.code stays at 167 values — no code is added, removed or moved. X-Tale-Api-Version answers 1.19.0. For the record, 0.5.38 moved the contract from 1.17.0 to 1.18.0 (task discussion reads and comment writes carry optional bodyByLocale snapshots), which its generated notes did not say.
  • GET /api/v1/teams (new) → {teams: [{id, name, member}]}, family none (a complete set, no cursor), tag Organization, any key holder; the Team schema is new.
  • GET …/runs/{runId}/ask (both scopes) and GET …/tasks/{taskId}/review: the nullable member is {allOf: [{$ref}], nullable: true}; PendingAsk.questions is {allOf: [{$ref: QuestionSet}], description}; ContactInput and ContactPatch.externalId carry nullable on each oneOf branch. A generated client that typed these nulls as non-null gets the right type on regeneration; nothing on the wire changes.
  • POST …/asks/{askId}: a blank answer is 400 EMPTY_ANSWER (was INVALID_BODY).
  • KnowledgeEntry.source is the enum chat | manual | api; the door writes api. PATCH /api/v1/knowledge-entries/{id} on a superseded row: data.activeId (when the topic still has an active row) and data.supersededBy.
  • POST / PATCH /api/v1/projects: a repeated team id collapses (was 400 PROJECT_SHARING_INVALID); the 400 text names INVALID_BODY for a teamIds past its maxItems; a no-op teamIds keeps updatedAt; PATCH {teamIds} on an archived project is 403 PROJECT_ARCHIVED (was 200). The teamIds descriptions point at GET /api/v1/teams.
  • PUT …/agents/{agentId}: an identical body writes nothing (updatedAt kept).
  • GET …/automations/{name}/runs (both scopes): answers a deleted automation's kept runs; its 404 is "a name no saved automation and no run of the organization bears". DELETE /api/v1/automations/{name} says so.
  • POST /api/v1/websites: a bare IP domain is 400 WEBSITE_DOMAIN_INVALID (was 201); WebsiteInput.domain says so.
  • DELETE /api/v1/products/{id} is documented as permanent — no trash, no restore, the name and externalId free at once; GET of a deleted product is 404 PRODUCT_NOT_FOUND (the wire is unchanged; the text said "trash").
  • KnowledgeHit.fusedScore: the description names the divisor — the best score possible given the legs that contributed candidates to this response (diagnostics.legs).
  • MCP (not in the OpenAPI document): an inexact integer anywhere in the body is -32600; get_knowledge passages gain documentId, corpus, chunkIndex, projectId; get_catalog core kinds and start_run on an undeployed version carry hints; list_runs {name} answers a deleted automation's runs.
  • generate:openapi on the release commit reproduces the shipped document; the contract fingerprint moved with the version.

Security

  • The chat archive and delete fences are the row's own predicate (#3428). The old check-then-act left a ten-to-thirty-millisecond window in which a send accepted by one request landed on a thread another request was deleting; the accepted turn then ran on a trashed thread — a billed turn nobody could read — and the poll answered 404 forever. The refusal is evaluated under the row lock now, on both sides of the race, so no turn opens on a thread that is gone or archived.
  • An inexact integer no longer rounds silently on the MCP door (#3428). A JSON-RPC id above 2^53 − 1 was echoed as its neighbour; a body value was stored altered. Both doors refuse the literal by path now. This is a tightening: an MCP client that sent 64-bit ids as numbers is refused where it was rounded before — send them as strings.
  • The web tier's JSON 404 echoes a request id only in the API's alphabet (#3428). A caller-supplied X-Request-Id is echoed when it is up to 255 letters, digits, _ or -, and replaced by a fresh UUID otherwise — the same rule the API applies — so the header never reflects arbitrary bytes.
  • The organization-slug 404 re-decodes before it echoes (#3428). The message quotes the header as the UTF-8 the caller sent; it is still capped at the slug length limit, answered without a lookup, and only for a value that cannot be a slug at all.
  • No dependency changes in this range, and no advisory is fixed. The @better-auth/oauth-provider advisory noted in 0.5.33 (CVE-2026-67332 / GHSA-p2fr-6hmx-4528, medium) remains open with its workaround in place; the 1.7.0 upgrade is still a separate dependency pull request.

Known issues

  • History is not rewritten. A knowledge entry the REST door wrote before this release keeps source: "manual"; nothing backfills api, because the row cannot tell which lane wrote it.
  • The robots verdict a scan reuses is up to a minute old by design; a site that changes its robots.txt within a minute of being registered is scanned under the earlier verdict once, and the next scan reads it again. A row stored before this release carries no sitemap list, so its next scan reads robots.txt once more and stores it.
  • The ask retraction is best-effort and forward-only. A card whose task was retired in the meantime has no timeline to retract on, and nothing retracts a "Question for you" card posted before this release.
  • The app's own archive stays unfenced by design: a turn that lands in a thread the app archived is settled by the worker. Only the REST door refuses.
  • GET /api/v1/teams is read-only by design. No operation on the machine surface creates, renames, staffs or deletes a team; that is the app or the identity provider.
  • The Team list is a complete set (family none), like the other unbounded lists recorded as contract debt; teams are few.
  • The chat content cap counts UTF-16 code units, so an emoji counts two; the message now says so, the limit itself is unchanged.
  • The nullable on a oneOf branch is the OAS 3.0 spelling; a 3.1-only reader still needs the document's declared version to read it.
  • 0.5.38 shipped with generated notes, so its four pull requests (#3424–#3427) have no known-issues record to carry here.
  • Unchanged from v0.5.37, where each is described in full: ledger rows booked before that release keep the subject they were booked under and a project agent can appear twice in Top assistants across the upgrade; app.usage_events is write-retired, not dropped; nothing in the schema forbids a door string in usage_ledger.user_id; the run list labels a keyed start Started by api-key:…; the GOV-F20 round is manual; llm nodes are unmetered and a run carries no usage or cost.
  • Unchanged from v0.5.36, where each is described in full: automation files: mounts and workflow document.* steps do not apply the team audience; a single-sign-on sign-in with an empty group list revokes nothing and a SCIM group replace overwrites hand-added members silently; the legacy team mirror columns stay; the three GIN indexes of migration 0109 were built without CONCURRENTLY; the team rounds NAV-F6, SET-F18, SET-F19, SET-F42, KNOW-F20, PROJ-F23, PROJ-F24 and CONV-F12 are manual; a team skill's teams list is validated only when it changes; REST Document.teamId stays as the deprecated single-team spelling.
  • Unchanged from v0.5.35, where each is described in full: a frame carries the signed-in session only from a same-site host page and the shell's embedding policy is the union across organizations; revoking a trusted-header key or turning the card off ends no session; the AUTH-F21–AUTH-F24, AUTH-B10 and SET-F41 rounds are manual; approvals have no REST twin; moving a folder has no door and documents already at the root stay there; the auto-retry resumes only a turn that announced its conversation handle; the Google Drive row counts a deployment app from either lane.
  • Unchanged from v0.5.34, where each is described in full: a managed deployment gets the organization-creator behaviour only once its specification declares organizations.creators and a new bundle is applied; the AUTH-B9 and AUTH-F20 rounds are manual; the creator list is matched against sign-in addresses.
  • Unchanged from v0.5.33, where each is described in full: the sign-up gate's first-boot race; the boot catch-up that marks provisioned accounts verified asks nobody; the break-glass administrator's password-rotation, single-sign-on-link and memory-adapter limits; the cross-scope webhook guard governs deliveries from that release on; a site's robots policy upgrades at its next scan; a scan waiting on render capacity takes longer by design; the governance pickers list only providers with an active credential; one dependency advisory is open.
  • Unchanged from v0.5.32, where each is described in full: the embedding pacing is proved against a controlled server, its bound is per Tale process, and minTokensPerSecond is a statement nothing verifies; the Kubernetes page's verified scope is one kind cluster, config-data needs RWX or a single node, and Tale ships no Helm chart.
  • Unchanged from v0.5.31, where each is described in full: a managed deployment picks up that release's proxy policy only when a newly prepared bundle is applied; the transcription setting is only as good as the organization's credentials; the six agent-turn fixes are bounded by the pinned Claude Code build they were read from; the 0.5.29 proxy change has been exercised live in TLS_MODE=letsencrypt only; the web tier's backend-URL default lives in the image, not in the generated compose; the scheduled-pack fix does not reach an automation an organization already has; a budget hold covers a turn's first round only; nothing backfills a task timeline.
  • 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; 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.
  • Cloud sync, left for later: there is still no Sync now action — the cadence is the fifteen-minute scan, so a reconnected account waits for the next run. A config whose owner leaves the organization is still deactivated silently by a different door, and a source-deleted item is still a status stamp with no bell.
  • Documents indexed before 0.5.27 keep one vector per repeated passage until they are re-indexed; the content hash is unchanged, so only an explicit retry-indexing (or a content change) re-embeds them.
  • The rail's navigation memory has had part of its manual round: the R5 round drove six EN/DE/FR desktop and phone cases covering parts of NAV-F16–NAV-F19; the remaining section, the second-account cases and NAV-B6–NAV-B9 are still unrun.
  • 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, answers asks and wires triggers.
  • 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 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 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.
  • Two bounded document readers still filter after their cut; both report an honest truncated, so a caller can tell the answer was cut.
  • 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 reverse proxy's network is IPv6-enabled — an operator item, documented on the Own Compose page.
  • Recorded as contract debt, each with its design in the ledger: a queued send is invisible on the message list until a worker opens it; a webhook delivery the deployed inputs schema refuses moves no trigger stamp; the MCP run_deployed tool keys its idempotency apart from start_run and REST; a page is fetched three to four times per scan; a cancelled run answers trace: null and effects: null where a failed run answers both; approvals have no REST twin; 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 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 stamps score: 0; no Idempotency-Key on the task start; no queue position on a queued send; a corrupt Office document 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; GET /notifications rows carry type as a free string and nothing pushes them to a machine caller; a skill keeps no version history on the machine door; the per-task circuit breaker is not built; the messages a conversation snapshot applied are readable only in the app; a run carries no usage or cost.

Migration notes

  • One migration, 0111 (0111_knowledge_entries_api_source.sql): drops the check constraint on app.knowledge_entries.source if it exists and re-creates it as CHECK (source IN ('chat', 'manual', 'api')). No column, no index, no backfill, no updated_at_ms moves. Adding the constraint validates the existing rows under the table's lock for the moment that takes — the table holds one row per fact version and every row already satisfies the wider check. The file is safe to re-run. It is rolling-deploy safe: the previous image writes only chat and manual, which the widened constraint still admits. The application database moves from 0110 to 0111; the knowledge database is unchanged, and Better Auth adds no column.
  • Migrations are tracked by file name. 0.5.38 shipped a second file with the 0108 prefix (0108_approvals_one_pending_conversation_draft.sql, one partial unique index); the boot applies every .sql file in name order once and records each by name, so a deployment crossing 0.5.38 runs that file at boot beside this release's 0111.
  • No environment variable is added or removed; .env.example is unchanged. No organization configuration file changes; the seed catalog is untouched. No scheduled job is added or retired, and no new audit action or error code appears.
  • No image in the stop-gated tier changes. The proxy and db images carry no source change, and the managed proxy policy the CLI renders is unchanged. A plain tale deploy is the whole upgrade: no --stop, no downtime window.
  • The platform image (the doors, the fence, the MCP endpoint, the crawler, the knowledge-entries badge) and the docs image (seven edited pages, each in English, German and French; no new page) carry source changes. The web, ui-docs, db, proxy, sandbox, sandbox-runtime, sandbox-buildkitd, sandbox-egress and sandbox-llm-gateway images carry no source change.
  • The CLI has no change of its own in this range, but the reference tree it embeds — the platform's shared modules, where the exact-integer parser, the crawl host policy and the contract version live — does, so the release executables are rebuilt and differ from 0.5.38; they report 0.5.39. Nothing in the range is new for an older CLI to refuse. A managed deployment should move its pinned CLI reference together with its platform reference, as always.
  • @tale/ui and @tale/marketing-ui are pinned by this release as the ui-v0.5.39 and marketing-ui-v0.5.39 tags on their snapshot branches; a consumer outside the monorepo installs "@tale/ui": "github:tale-project/tale#ui-v0.5.39". Neither package changes in this range, so both tags are content-identical to their 0.5.38 predecessors.

Upgrading

  • On the 0.5 line (0.5.0 – 0.5.38):

    tale update
    tale deploy

    Migration 0111 is applied at boot. Nothing in this release needs --stop. A deployment crossing 0.5.38 runs that release's 0108_approvals_one_pending_conversation_draft.sql at boot as well; one crossing 0.5.37 runs migration 0110, one crossing 0.5.36 runs migration 0109, one crossing 0.5.35 runs migration 0108 (0108_trusted_header_keys.sql) and Better Auth's session column, and one crossing 0.5.33 runs migration 0107. A deployment crossing from a version older than 0.5.29 should read that release's notes, which do: its proxy image change is only applied by a --stop deploy.

  • Before you upgrade, check what your integrations assume. A sync that re-asserts a project's audience on every pass will see updatedAt stop moving on a no-op — a client that read the stamp as a heartbeat has to stop. A client that branched on INVALID_BODY for a blank ask answer now sees EMPTY_ANSWER. A consumer of GET /api/v1/knowledge-entries that mapped source onto two values has to admit api. An MCP client that sent 64-bit ids as numbers is refused now — send them as strings. A caller that relied on GET …/automations/{name}/runs answering 404 as "the automation is gone" should read GET /api/v1/automations/{name} instead, which still answers 404 for a deleted automation.

  • 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. 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.

What's Changed

  • fix(platform): close the 2026-09-19 round-K API evaluation findings by @larryro in #3428

Full Changelog: v0.5.38...v0.5.39