Skip to content

Tale v0.5.37

Choose a tag to compare

@larryro larryro released this 19 Sep 13:44
0acb801

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:… or api-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> and api-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.code values. X-Tale-Api-Version still answers 1.17.0.
  • startedBy on 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 reads startedBy has to change.
  • A run still carries no usage block; 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_ledger rows whose user_id equalled the subject's id, so the rows the automation lane had booked under user:<id> and api-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 retired app.usage_events rows 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 a trigger:<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 with ORG_FORBIDDEN — but the refusal rested on that coincidence. The store reads the actor through the one starter parser now and answers UNAUTHENTICATED before the lookup.
  • 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. Ledger rows booked before this release keep the subject they were booked under: an automation's spend still sits on a user:… or api-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_events is write-retired, not dropped. The previous image still writes it during a rolling upgrade, so the DROP TABLE waits 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; a CHECK constraint lands NOT VALID in 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.
  • llm nodes 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 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.

Migration notes

  • One migration, 0110 (0110_run_billing_subject.sql): adds a nullable api_key_id text column to app.automation_runs and to app.sandbox_session_ops. Both are ADD 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 no updated_at_ms moves. 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.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 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-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 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/ui and @tale/marketing-ui are pinned by this release as the ui-v0.5.37 and marketing-ui-v0.5.37 tags 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: its proxy image change is only applied by a --stop deploy.

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

  • feat(platform): book agent spend under the person who started the run by @larryro in #3423

Full Changelog: v0.5.36...v0.5.37