Tale v0.5.37
0.5.37 carries 1 merged pull request. Every billable call Tale makes now names the person who asked for the work. The usage ledger's subject is a bare user id — the member who sent the chat message, or the one who started the agent run, whichever door they came through — or one sentinel row, Automations (triggers), for a run a schedule, a webhook or an event started, which nobody is responsible for. The automation lane used to copy the run's door (user:…, api-key:…, trigger:…) into the ledger instead, so Per-user usage listed pseudo-users beside real ones, personal and team limits never saw automation spend, and a run started with an API key was billed to no key at all. A keyed run now books to its key as well, so a key's own budget cap sees what the integration behind it costs; a project agent books under its stable id and the page resolves its name when it reads. The rule is written down twice: a new documentation page, How usage is counted, for administrators and members in English, German and French, and a contract README for the next lane that spends. One migration (0110) adding two nullable columns, no backfill and no environment change; the API contract is unchanged at 1.17.0, and no image in the stop-gated tier changes, so the upgrade is tale update followed by a plain tale deploy.
Highlights
Every billable call is booked under a person (#3423)
Three lanes wrote to the same usage ledger and disagreed about what its user_id column meant. Chat and project-agent turns wrote a bare user id. The automation lane copied automation_runs.started_by — the door that started the run, in the format the REST contract publishes on startedBy (user:<id>, api-key:<id>, trigger:<triggerId>) — verbatim into the ledger. The consequences ran through every reader: Per-user usage showed rows named api-key:… and user:…, which resolve to no member and so were labelled with the raw string, and one person's chat and automation spend sat on two separate rows; the budget gate looked a door string up as a member, found none, and measured the turn against the organization's default personal triple in a usage pool of its own, so a member's real personal and team caps never saw a single automation run; erasure deleted the bare-id rows and left the prefixed ones behind; and a run started with an API key recorded nothing against that key, because the run row never knew which key had authenticated it.
The rule is now one sentence with one owner: every app.usage_ledger row names a person by bare user id, or the single sentinel __automation__ for spend nobody is responsible for, plus the API key when one authenticated the start. services/platform/backend/domains/governance/README.md states it as eight numbered rules with the lane table, the reader table and the guards behind each; .agents/repo.md carries it as a repository boundary. The door format is untouched — the REST contract, erasure and the trigger fire ledger all read started_by — but exactly one module parses it, lib/shared/run-starter.ts, and managed agent turns resolve their subject in exactly one place, resolveSessionOpAttribution, which both the reservation and the settlement call so a turn is measured and booked against the same subject.
A run started with an API key books to that key (#3423)
Settings > Governance has been able to cap what one API key may spend since budget rules shipped, and the documentation has promised that the runs a key starts count toward it. They did not: a chat message sent with a key was booked to it, but a run start had nowhere to record the key — app.automation_runs had no column for it — so a key-driven integration could start automation runs all day against a cap that never moved. Migration 0110 adds api_key_id to app.automation_runs and to app.sandbox_session_ops; the REST run start, the REST task-workflow starts (including the act-as lane and the agent-mention comment), and the MCP endpoint's run tools hand the authenticating key's id to the run, the reservation stamps it on the op, and the settlement books it beside the person. Both columns are nullable and are set by the keyed doors alone: a start from the run list, the builder, a chat capability or a trigger carries none.
The member's own limits apply as well. A keyed request is measured against the personal, team and role caps of the member the key acts for — the key holder, or the member named on an act-as request — and against the key's own cap; the strictest of them refuses first, as How rules combine already described for rules that overlap.
Trigger-started runs share one row, and never look like a member (#3423)
A run a schedule, a webhook or an event started has no person behind it. It books under __automation__, and every read side treats that subject as a bucket rather than a user: Per-user usage shows it as Automations (triggers) (translated into German and French), it is excluded from the Active users count, and the budget gate binds only the organization's caps to it — and the key's, on the rare keyed path — because there is no person whose personal, team or role cap could apply. A short note under the table names the rule so an administrator reading the page does not have to infer it, and the member's Settings > Usage now says the runs they start count toward their limits whichever way they started them.
A project agent books under its id, not its display name (#3423)
The agent_slug axis mixed identifiers and labels: a chat assistant booked under its slug and an automation under its name, but a project agent booked under its display name, which an administrator can change at any time — so renaming an agent split its history in two and two agents sharing a name merged theirs. A project agent now books under project_agents.id, a value that never changes, and the Top assistants table resolves ids to names when it reads. The rule generalizes: agent_slug is a stable identifier, and a reader that wants a label looks it up.
How usage is counted, written down (#3423)
A new page, How usage is counted (Governance > How usage is counted, in English, German and French), answers the question administrators and members actually ask: what counts as usage, who each kind of work counts against, which limits apply to it, and where it shows up in Usage analytics. A table walks every lane — a chat reply and the model call that titles a new chat, an agent run on a task, an automation run someone started, an automation run a trigger started, voice output, transcription, a metered connector call — with the person it counts against, the API key it also counts toward when one was used, and the row it appears on. Three situations that confuse people get their own answers: a teammate who mentions your agent in a task comment spends their own allowance, not yours; a nightly scheduled automation lands on the Automations (triggers) row and only an organization limit can stop it; and a retry continues the run its starter kicked off. The page is linked from Usage analytics, Policies and limits, API keys and the member's Preferences.
Behaviour changes
- A member's personal and team budget caps now count the automation runs they start. Before this release the automation lane's spend was measured against the organization's default personal limits in a pool of its own, so a member with a personal cap could start automation runs past it. An organization that runs automations under personal caps should review those rules before upgrading — see Upgrading.
- A run a trigger started is no longer measured against a personal limit. It has no person, so only the organization's caps (and a key's, where one is involved) can refuse it. Set an organization cost or request limit when trigger-started runs need a ceiling.
- A run started with an API key counts toward that key's budget cap, beside the personal, team and role caps of the member it acts for.
- Per-user usage no longer shows
user:…orapi-key:…rows for work done from this release on; trigger-started runs sit on one Automations (triggers) row, which does not raise Active users. Rows booked before the upgrade keep the form they were written in. - Top assistants shows a project agent under its current name, resolved when the page is read, rather than under whatever name it carried when the spend was booked; renaming an agent no longer splits its history from here on.
- The member's Settings > Usage description now says that the agent runs they start count toward their limits whichever way they started them.
- Erasing a member now also removes the ledger rows the automation lane booked under
user:<id>andapi-key:<id>before this release; only the bare-id rows were removed before. - The chat lane stops writing
app.usage_events, the per-turn row it kept beside the ledger. Nothing ever read it; erasure and retention still sweep the rows already there. - A project-agent or automation run whose starter cannot be parsed falls back to the subject the reservation stamped on the op row, rather than booking the unreadable value.
API contract changes
- None. The OpenAPI document is byte-identical to 0.5.36: 1.17.0, 85 paths, 133 operations, 62 schemas, 167
Error.codevalues.X-Tale-Api-Versionstill answers1.17.0. startedByon a run keeps its door format (user:<id>,api-key:<userId>,trigger:<triggerId>) — the attribution fix happens at the ledger boundary, never by rewriting a door field, so no client that readsstartedByhas to change.- A run still carries no
usageblock; that remains contract debt with its design recorded in the ledger.
Security
- Erasure covers the door-form rows it used to miss (#3423). A data-subject erasure deleted
app.usage_ledgerrows whoseuser_idequalled the subject's id, so the rows the automation lane had booked underuser:<id>andapi-key:<id>— the same person's spend, in the door's spelling — survived the erasure. The pass now deletes all three forms, and sweeps the subject's retiredapp.usage_eventsrows as well. - A key's budget cap now binds what the key starts. A cap on an API key was enforceable only for chat sends; the runs a key started were invisible to it. This is a tightening: an integration that starts runs with a capped key can now be refused where it was not before.
- An engine actor that names nobody is refused at the parse (#3423). The automation dispatch store split its actor string on the first
:and treated whatever followed as a user id, so atrigger:<triggerId>or an unrecognized door reached the membership lookup as a candidate user. No trigger id is a user id, so nothing is known to have passed — the lookup refused it withORG_FORBIDDEN— but the refusal rested on that coincidence. The store reads the actor through the one starter parser now and answersUNAUTHENTICATEDbefore the lookup. - 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. Ledger rows booked before this release keep the subject they were booked under: an automation's spend still sits on a
user:…orapi-key:…row, and a project agent's on its display name at the time. The decision was deliberate — new data has to be clean, and a rewrite of a period bucket cannot be undone. A usage window that spans the upgrade therefore shows both forms, and a project agent can appear twice in Top assistants: once as the old name-keyed bucket and once as the new id-keyed one resolved back to the same name. app.usage_eventsis write-retired, not dropped. The previous image still writes it during a rolling upgrade, so theDROP TABLEwaits for a later release; erasure and retention cover its rows until then.- Nothing in the schema forbids a door string in
usage_ledger.user_id. The rule is enforced by the resolver and its tests; aCHECKconstraint landsNOT VALIDin a later release, once no image in a roll can still write the old form. - The run list still labels a keyed start
Started by api-key:…— the raw door string, unchanged and cosmetic; it names the same door the contract publishes. - The browser round for the usage page (
GOV-F20) is manual: the subject per lane, the sentinel, the key, the stamp fallback, the impersonal budget subject and the name resolution are proved by unit tests and by a backend integration probe that settles a trigger-started and a keyed workflow op against real Postgres and reads both ledger rows. llmnodes in an automation are still unmetered and a run still carries no usage or cost; this release changes who spend is booked under, not which nodes book it.- 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.
Migration notes
- One migration, 0110 (
0110_run_billing_subject.sql): adds a nullableapi_key_id textcolumn toapp.automation_runsand toapp.sandbox_session_ops. Both areADD COLUMN IF NOT EXISTS, so the file is idempotent; both are columns the previous image neither reads nor writes, so it keeps serving while the migration applies. No backfill, no index, no constraint, and noupdated_at_msmoves. The application database moves from 0109 to 0110; the knowledge database is unchanged, and Better Auth adds no column. - Ledger history is not rewritten. No statement touches
app.usage_ledger; rows booked before this release keep their subject, and the readers that need to cover both forms (erasure) name them explicitly. - 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 ledger subject, the doors, the budget gate, the usage page) and the docs image (the new How usage is counted page plus four edited pages, each in English, German and French) 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 sentinel and the starter parser live — does, so the release executables are rebuilt and differ from 0.5.36; they report 0.5.37. 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.37andmarketing-ui-v0.5.37tags on their snapshot branches; a consumer outside the monorepo installs"@tale/ui": "github:tale-project/tale#ui-v0.5.37". Neither package changes in this range, so both tags are content-identical to their 0.5.36 predecessors.
Upgrading
-
On the 0.5 line (0.5.0 – 0.5.36):
tale update tale deploy
Migration 0110 is applied at boot. Nothing in this release needs
--stop. A deployment crossing 0.5.36 runs that release's migration 0109 at boot as well; one crossing 0.5.35 runs migration 0108 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, review your budget rules if you run automations. Personal and team caps now count the automation runs a member starts, where before that spend was measured against the organization's default personal limits in a pool of its own. A member whose work is mostly automation runs can reach a personal cap that never used to bind them. Trigger-started runs move the other way: no personal cap binds them any more, so give the organization a cost or request limit if they need a ceiling. Settings > Governance > Policies and limits holds both, and How usage is counted explains which rule applies to which work.
-
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.36...v0.5.37