Tale v0.5.39
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}witharchived: trueandDELETE …/threads/{id}refuse from the row lock: a turn that claimed the thread between the door's read and its write is 409CHAT_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 404THREAD_NOT_FOUNDor 409CHAT_THREAD_ARCHIVEDinstead of 202.GET /api/v1/teamsis new:{teams: [{id, name, member}]}, a complete set, any key holder,ETagand 304 like every read.POST …/asks/{askId}answers 400EMPTY_ANSWERfor a blank answer (wasINVALID_BODY).GET …/automations/{name}/runsand the project twin answer a deleted automation's kept runs (was 404); MCPlist_runs {name}the same.POST /api/v1/projectsandPATCH /api/v1/projects/{id}: a repeated team id collapses to one (was 400PROJECT_SHARING_INVALID); the audience the project already carries is a no-op that keepsupdatedAtand writes no audit row;PATCH {teamIds}on an archived project is 403PROJECT_ARCHIVEDunless the same body restores it (was 200 with a real change).PUT …/projects/{id}/agents/{agentId}with the stored configuration writes nothing and keepsupdatedAt.POST /api/v1/websitesrefuses a bare IP address asdomainwith 400WEBSITE_DOMAIN_INVALID(was 201 and atls_errorscan).POSTandPATCH /api/v1/knowledge-entrieswritesource: "api"; the superseded 409 carriesdata.activeIdanddata.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
-32600naming its path;get_knowledgepassages carrydocumentId,corpus,chunkIndexandprojectId;get_catalog {kind: <core kind>}answers{node_types: [], hint};start_runon an undeployed version namesrun_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.txtand the homepage before the first scan is queued, persists the robots verdict with its sitemaps, and paces the homepage read by the site'sCrawl-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 carriesX-Request-Id(a caller's own when it is letters, digits,_or-up to 255 characters, else a fresh UUID) andCache-Control: no-store; it carries noX-Tale-Api-Version, because that tier answers no version of the contract. HEADon/llms.txt,/llms-full.txt,/robots.txtand the app shell carries theGET'sContent-Length(was0).- A non-ASCII
X-Organization-Slugthat names no organization is echoed back as the UTF-8 the caller sent (tälé, nottã¤lã©), still capped and still without a lookup. DELETE /api/v1/browser-sessions/importis 405 withAllow: POST, OPTIONS(it used to reach the pool gate as a delete of a session calledimport, andOPTIONSadvertisedDELETE).- The chat send's
contentcap 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 inbacklog,?version=latest, the guarded skill delete's 404,source: "api"anddata.activeId, products without a trash, the onboarding tables'404 ORG_SLUG_INVALIDrow, the deploy gate (a version with a failing test cannot be deployed; one with none can), the MCP header refusal, passage fields,list_runsexception andget_cataloghint, 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.codestays at 167 values — no code is added, removed or moved.X-Tale-Api-Versionanswers1.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 optionalbodyByLocalesnapshots), which its generated notes did not say. GET /api/v1/teams(new) →{teams: [{id, name, member}]}, familynone(a complete set, no cursor), tag Organization, any key holder; theTeamschema is new.GET …/runs/{runId}/ask(both scopes) andGET …/tasks/{taskId}/review: the nullable member is{allOf: [{$ref}], nullable: true};PendingAsk.questionsis{allOf: [{$ref: QuestionSet}], description};ContactInputandContactPatch.externalIdcarrynullableon eachoneOfbranch. A generated client that typed thesenulls as non-null gets the right type on regeneration; nothing on the wire changes.POST …/asks/{askId}: a blankansweris 400EMPTY_ANSWER(wasINVALID_BODY).KnowledgeEntry.sourceis the enumchat | manual | api; the door writesapi.PATCH /api/v1/knowledge-entries/{id}on a superseded row:data.activeId(when the topic still has an active row) anddata.supersededBy.POST/PATCH /api/v1/projects: a repeated team id collapses (was 400PROJECT_SHARING_INVALID); the 400 text namesINVALID_BODYfor ateamIdspast itsmaxItems; a no-opteamIdskeepsupdatedAt;PATCH {teamIds}on an archived project is 403PROJECT_ARCHIVED(was 200). TheteamIdsdescriptions point atGET /api/v1/teams.PUT …/agents/{agentId}: an identical body writes nothing (updatedAtkept).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 IPdomainis 400WEBSITE_DOMAIN_INVALID(was 201);WebsiteInput.domainsays so.DELETE /api/v1/products/{id}is documented as permanent — no trash, no restore, the name andexternalIdfree at once;GETof a deleted product is 404PRODUCT_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_knowledgepassages gaindocumentId,corpus,chunkIndex,projectId;get_catalogcore kinds andstart_runon an undeployed version carry hints;list_runs {name}answers a deleted automation's runs. generate:openapion 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
idabove 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-Idis 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-provideradvisory 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 backfillsapi, 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.txtwithin 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 readsrobots.txtonce 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/teamsis 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
Teamlist is a complete set (familynone), like the other unbounded lists recorded as contract debt; teams are few. - The chat
contentcap counts UTF-16 code units, so an emoji counts two; the message now says so, the limit itself is unchanged. - The
nullableon aoneOfbranch 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_eventsis write-retired, not dropped; nothing in the schema forbids a door string inusage_ledger.user_id; the run list labels a keyed startStarted by api-key:…; theGOV-F20round is manual;llmnodes 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 workflowdocument.*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 withoutCONCURRENTLY; the team roundsNAV-F6,SET-F18,SET-F19,SET-F42,KNOW-F20,PROJ-F23,PROJ-F24andCONV-F12are manual; a team skill'steamslist is validated only when it changes; RESTDocument.teamIdstays 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-B10andSET-F41rounds 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.creatorsand a new bundle is applied; theAUTH-B9andAUTH-F20rounds 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
minTokensPerSecondis a statement nothing verifies; the Kubernetes page's verified scope is one kind cluster,config-dataneeds 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=letsencryptonly; 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-ccColombian 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_searchembedding calls inside a harness turn are unmetered; the product edit dialog cannot clear a field; the app's skill editor still carries the retiredprivatevisibility. - 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 andNAV-B6–NAV-B9are 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
visionmodel reads an image over REST only on a thread the app continued with an image attachment; the design of anattachmentsfield on the send is recorded as contract debt. - No REST door authors or deploys an automation —
POST /automationsanswers 405 by design. Build and deploy in the app, or over the MCP endpoint'ssave_automationanddeploy_automation; the REST key lists, reads, runs, answers asks and wires triggers. - The
x-tale-paginationextension is a declaration on the OpenAPI document; generated clients that do not read vendor extensions still branch on the two cursor names untilcursoris retired. - The app's zip upload of a skill bundle rewrites the bundle and moves
updatedAteven when the zip is byte-identical, wherePUT /skills/{slug}writes nothing. - A tool call the reply cap cut keeps
input: {}on the storedtool-callpart; 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
ip6tablesand 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
inputsschema refuses moves no trigger stamp; the MCPrun_deployedtool keys its idempotency apart fromstart_runand REST; a page is fetched three to four times per scan; a cancelled run answerstrace: nullandeffects: nullwhere 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 deployedinputsschema admits a delivery; an exhaustedrepeatUntilis only a trace note;Websitecarries noscanStartedAtand 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 stampsscore: 0; noIdempotency-Keyon the task start; no queue position on a queued send; a corrupt Office document still fails asindexer_errorand is retried five times where a PDF landsmalformed; no/.well-known/security.txt; no changelog feed on tale.dev; no SDK, collection or per-code table beyond theError.codeenum;GET /notificationsrows carrytypeas 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 onapp.knowledge_entries.sourceif it exists and re-creates it asCHECK (source IN ('chat', 'manual', 'api')). No column, no index, no backfill, noupdated_at_msmoves. 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 onlychatandmanual, 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
0108prefix (0108_approvals_one_pending_conversation_draft.sql, one partial unique index); the boot applies every.sqlfile 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.exampleis 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
proxyanddbimages carry no source change, and the managed proxy policy the CLI renders is unchanged. A plaintale deployis 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-egressandsandbox-llm-gatewayimages 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/uiand@tale/marketing-uiare pinned by this release as theui-v0.5.39andmarketing-ui-v0.5.39tags 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's0108_approvals_one_pending_conversation_draft.sqlat 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: itsproxyimage change is only applied by a--stopdeploy. -
Before you upgrade, check what your integrations assume. A sync that re-asserts a project's audience on every pass will see
updatedAtstop moving on a no-op — a client that read the stamp as a heartbeat has to stop. A client that branched onINVALID_BODYfor a blank ask answer now seesEMPTY_ANSWER. A consumer ofGET /api/v1/knowledge-entriesthat mappedsourceonto two values has to admitapi. An MCP client that sent 64-bit ids as numbers is refused now — send them as strings. A caller that relied onGET …/automations/{name}/runsanswering 404 as "the automation is gone" should readGET /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 thesetup-cliaction andbun run --filter @tale/cli buildproduce 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, passlinux-baseline: 'true'to thesetup-cliaction 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 withbun run build:linux-baselineintools/cli.
What's Changed
Full Changelog: v0.5.38...v0.5.39