REFERENCE

What changed

Every change to the hosted platform that you can see from outside: the widget, the /v1 API, and the agent's behaviour. Newest first.

The platform ships continuously, and your embedded widget updates itself. This page is how you find out what moved before it surprises you in production. Anything that can break a working integration is marked, and says what to do about it.

What is a contract, and what is not

The line matters, because one side of it is safe to build on and the other side moves without warning.

Stable. These do not change without an entry on this page telling you so:

  • The --era-* CSS custom properties and the ::part() names listed in the Widget API reference.
  • The data-* attributes on the script tag, the window.ERAConfig shape, and the methods on window.ERA.
  • The /v1 HTTP surface, its request and response fields, its SSE event types, and the error codes in the Error reference.
  • The two fenced-block conventions the agent emits: choices and era-ui.

Not a contract. Everything inside the shadow root that is not a variable or a part: element ids such as #panel, #ft-tasks and #inp, class names such as .bbl, the DOM structure, the CSS rules themselves, and the wording of any string the widget renders. The shadow root is open, so overriding those works — and it will keep working right up until the release where it silently stops. There is no major-version boundary protecting them, because there is no version. Two of the entries below are exactly this failure.

There is no version number, and nothing to pin. The widget ships from one fixed URL, /static/widget.js, served with Cache-Control: no-cache, so every page load revalidates and gets the current build. The file reports no version of its own — the only way to tell which build a page is running is to diff the file. If you must freeze a build, host a copy of widget.js yourself and set data-base-url to your Agentifys origin, because the widget otherwise derives its API base from the script's own src and a self-hosted copy would call your own domain instead. You then stop receiving fixes, security fixes included.
Entries before 13 September 2026 are reconstructed. This page did not exist while they shipped; they were rebuilt afterwards from the repository history. They are accurate about what changed, but they are not a complete record of every fix, and each date is the date the change was committed, not the moment it reached production.

13 September 2026

The agent knows what time it is, and where the user isNew

The model was never told the date or the time. It had to invent "now" to turn "tomorrow at 9" into a stored schedule, and an omitted timezone became UTC in silence. Every turn now carries a Current time block: the current UTC time and, when a timezone is known, the user's local time and zone.

The widget reads Intl.DateTimeFormat().resolvedOptions().timeZone once at boot and sends it as a top-level client_tz field on the chat request. It is deliberately not put inside user_context: that object is HMAC-signed by your backend, and adding to it in the browser would invalidate the signature.

Resolution order is most-trusted first: timezone inside a verified user_context, then client_tz. Both are validated against the IANA database before they reach the prompt, and anything that is not a real zone name is dropped. Etc/GMT±N is refused on both paths — its sign is inverted and it ignores daylight saving. With neither, the agent is told the timezone is unknown and is instructed to ask before it schedules anything.

What to do: If you call POST /v1/chat directly rather than through the widget, add client_tz to the body (an IANA name, 64 characters maximum) or put timezone in the signed user_context. If you proxy the widget's requests through your own backend, confirm the proxy preserves body fields it does not know about, or the field never arrives.

Scheduling refuses a timezone it does not recogniseChanged

"Riyadh", "GMT+3" and "AST" used to be stored as UTC without a word, so a 9am task fired at noon. The scheduling tool now returns an error naming the bad zone and the agent relays it, instead of creating the task.

Two adjacent bugs went with it. A timezone-only update was a silent no-op — "make that Riyadh time" answered "updated" and changed nothing; it now re-anchors a recurring task, and explicitly refuses on a one-time task, whose original local time is gone once it has been stored as an instant. An update that changed nothing at all also used to report success.

A scheduled run can no longer reach a write it is not authorised forAction needed

A run that tried a write action outside the task's pre-authorized allowlist appended an apology to its output and recorded ok. A run that did nothing was stored, and shown to the user, as having worked. Unattended runs now drop every write tool the task was not pre-authorised for before the model is given its tool list, so the model cannot request one. The run completes as ok with a reply that says what it could not do, and the schedule still advances.

A blocked status was added alongside this for a run stopped at an unauthorised write. Because the tool is now removed before the model sees it, that state is not reachable and no run is recorded with it today. Treat it as defined but unused.

GET /v1/scheduled-tasks also returned last_result from the run's output only, and a failed run has no output, so a broken task looked idle rather than broken. Each task now carries last_error alongside last_run_status, and the widget's My-tasks panel renders it in amber when the last run was not ok.

What to do: If you read the scheduled-tasks API, surface last_error rather than inferring health from last_result alone. To let a task perform a write unattended, add that action to the task's allowed actions — otherwise the run will report that it could not do it. See the Scheduled tasks API.

Per-user memory was dead on every non-Anthropic projectFixed

Memory asked for a provider key named anthropic by name and hardcoded a Claude model. On a bring-your-own-OpenAI or bring-your-own-Google project the effect was total: no session was ever captured, no summary ever formed, and nothing was ever injected — while the console showed memory as enabled. It now resolves the project's highest-priority key and summarises with that key's own model, so Anthropic, OpenAI and Google all work.

Capturing sessions no longer requires a key at all, so a project that adds one later still has its history. Nothing to change on your side — affected projects start accumulating memory from the next turn. History from before the fix was never captured and cannot be recovered.

Allowed origins has one home in the consoleConsole only

The admin menu is regrouped by what you are doing, and the widget origin allowlist is no longer duplicated across two panels. Nothing in the API, the widget or the agent changed.

8 September 2026

The agent can point at your pageNew

Instead of describing where a control is, the agent can put a ring on the real element. It ends a reply with a fenced era-ui block; the widget parses the block out, runs it, and never renders it as text.

text
It is in Settings, here:

```era-ui
{"action":"point","target":"invite-teammate","label":"Invite a teammate"}
```

It points, it never clicks. The spotlight ignores pointer events and clears itself on a gesture, on resize, on the next message, on Escape and on a timeout, so it cannot trap anyone. Two things turn it on: mark the elements you want reachable with data-era-id, and list those ids under a heading Screen elements you can point at in your project's system prompt. The agent may only name ids from that section — no CSS selectors — so until you add it nothing is highlighted and replies describe the path in words. Your own ERA.point() calls still accept any selector. Turn the whole feature off with data-point-at="off".

This shipped broken and was fixed the same day. Nothing told the model the block existed, so no agent ever emitted one; and the spotlight registered its dismiss-on-scroll listener before scrolling the target into view, so the widget's own smooth scroll cleared the ring before it was drawn — pointing only worked for targets already on screen. If you evaluated this feature on 8 September and concluded it did not work, evaluate it again.

"Ask AI" selection pillNew

Select text anywhere on your page and a pill appears; clicking it opens the panel with the passage already quoted in the composer. It is opt-in, via data-select-ask="on", and deliberately so: it watches selections across your whole page and moves the selected text into a transcript your admins can read, which is not something an embed should acquire by picking up a newer build. Styled through ::part(ask).

Dictation, read-aloud, and a command paletteNew

A microphone writes speech into the composer and a speaker toggle reads answers aloud, buffered by sentence so it does not stutter on token deltas. Both are browser APIs: no backend, no cost, no provider dependency. Both are on by default where the browser supports them and hidden entirely where it does not — turn them off with data-dictation="off" and data-read-aloud="off". New parts: ::part(mic) and ::part(speak).

The command palette is opt-in via data-command-key (for example data-command-key="k" for Ctrl/Cmd+K) and can also be opened with ERA.openPalette(). It is off unless you ask for it, and its hotkey is scoped to the widget rather than the document, because a global shortcut fires while someone is typing anywhere in your app. It overlays the panel rather than replacing a view, so panel.style.display still means "the drawer is open". Styled through ::part(palette).

What to do: The mic and speaker buttons are new children of the footer input row, between the textarea and the send button. If your skin positions that row by child count or with :last-child, check it. Styling those buttons by part rather than position survives the next change.

Real tool labels in the status lineChanged

While a tool runs, the status line now prefers a server-sent progress_label or display_name over the rotating "Thinking… / Analyzing…" filler, which was visible on every single turn. A label configured in the console needs no widget change to appear.

ERA.destroy(), and the listener leak it fixesAction needed

The widget added listeners to your document that were never removed, so an app that rebuilt the widget on every sign-in accumulated them. ERA.destroy() removes the widget, stops the mic, aborts any in-flight stream, takes off every listener it added, and deletes window.ERA. Re-injecting the script now tears down the previous instance first, so mounting twice is safe.

What to do: On sign-out call ERA.clear() and then ERA.destroy(), in that order — the session id is stored per project, not per user, so the next person on that browser inherits the conversation otherwise. Deleting #era-widget from the DOM yourself does not clean up: the document listeners stay, each holding a detached tree alive. The full teardown is in Embed the widget.

7 September 2026

One byte-exact identity signer — the published recipe was wrongBreaking

Agentifys published a hand-rolled canonical-JSON signer in four places and not one of them was byte-exact with the verifier it had to match, json.dumps(ctx, sort_keys=True, separators=(",", ":")) with CPython's default ensure_ascii=True. Three divergences, each of which signs a different string than the server verifies:

  • Every non-ASCII character must be escaped to \uXXXX. One published copy omitted this entirely, so an ordinary Arabic name failed to verify — invisible in all-ASCII test data, and the common case in production.
  • Keys sort by Unicode code point, not by UTF-16 code unit. Object.keys(o).sort() does the latter, and the two disagree above the BMP.
  • Only integers are safe. Python renders 1e-7 as 1e-07 and JSON.stringify does not.

Every one of them surfaces as IDENTITY_SIGNATURE_INVALID, which reads as a permissions fault rather than an encoding one, so the time goes into auditing the wrong thing. There is now a single signer, cross-checked against CPython for Arabic values, astral values, astral keys and nested structures, and a conformance vector set plus a regression test keep it that way.

javascript
// The escaping step that was missing. Apply it to every string, keys included.
function enc(v) {
  return JSON.stringify(v).replace(/[^ -~]/g,
    c => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
}

What to do: If you copied the signing code before 7 September 2026, replace it with the current snippet in Identify your users. An all-ASCII integration keeps working, which is why this can sit undetected until your first user with a non-Latin name signs in. Sign integers only; send any other number as a string.

Identity errors say what actually fixes themFixed

A signature is valid for 300 seconds, so an expired one is routine on a page that stays open. The widget had no mapping for the identity error codes, so an expired signature fell through to the 401 catch-all and every signed-mode end user was told "This project isn't configured yet." about a page that needed a reload. IDENTITY_SIGNATURE_EXPIRED, IDENTITY_SIGNATURE_INVALID and IDENTITY_SIGNATURE_REQUIRED now say the session expired and to reload; IDENTITY_NOT_CONFIGURED and SIGNED_IDENTITY_REQUIRED are documented as project settings faults, not caller faults, even though both are returned as 401 or 403.

What to do: On a long-lived page, re-sign on a timer and assign the result to window.ERAConfig.user. The widget re-reads that object on every request, so a fresh signature takes effect without a reload.

New skin, and the theming contract that goes with itChanged

The panel was restyled: a minimal header on the panel background with a hairline and an accent dot instead of a solid accent bar, a 372×540 panel with a 22px radius, an airier message list, rounder bubbles, a pill input with a soft focus ring, and circular send and attach buttons.

No id or class was renamed and no markup was restructured, so overrides that existed kept matching. What arrived with it is the supported way to do this instead: the --era-* custom properties for colours, radii and font, and ::part() handles for launcher, panel, header, title, close, messages, footer, input, send, attach, welcome, option, banner, tasks and the message bubbles. The 8 September release added mic, speak, ask, spotlight and palette on top.

What to do: If your skin pins the widget's look with rules against internal ids, this is the release to move it onto variables and parts. Set your brand colour on data-primary-color as well as in CSS: the widget converts that hex to r,g,b once at boot and writes the channels literally into its translucent rules, so re-pointing --era-accent in CSS alone moves the solid accents but leaves every glow on the old colour. The full variable and part tables are in the Widget API reference.

onMessage fires for session, and the close button is no longer clippedFixed

session was the one stream event ERA.onMessage() swallowed, so a listener could not see the session id the server assigned. It now fires for it. Separately, the header close button's icon was the only unsized SVG in the widget, so it fell back to the 300×150 replaced-element default and was clipped by the panel's overflow: hidden. An integrator had already patched that one himself.

1 September 2026

Markdown tables render in the widgetNew

The widget's markdown renderer and the console renderer both render GFM pipe tables. Before this, an answer containing a table showed raw pipes.

Widget updates actually reach embedded pagesFixed

/static/widget.js is now served with Cache-Control: no-cache, so a browser revalidates it on every load — cheaply, via ETag — instead of holding a stale copy for days. The consequence cuts both ways: fixes reach your users on their next page load, and you cannot pin a build. See the note at the top of this page.

31 August 2026

Public documentation siteNew

The site you are reading: a pre-rendered marketing, guides and API-reference set, each route with its own metadata and canonical URL. Before this, the only documentation an integrator could reach was the in-console integration guide.

30 August 2026

Console fixesConsole only

An /admin deep link survives a hard refresh instead of 404ing. MCP servers became editable after creation, an empty auth token is refused rather than saved, and the setup checklist updates live.

26 August 2026

The choices block became an explicit contractNew

The agent ends a reply with a fenced choices block holding a JSON array of 2 to 6 short strings, and the widget renders them as buttons; tapping one sends it as the next message. Before this the widget guessed at trailing numbered lists, which turned ordinary lists of data into buttons. It now only converts a real trailing choice list. Disable the whole thing per embed with data-options="off", and style the buttons through ::part(option).

Google (Gemini) as a third bring-your-own-key providerNew

Alongside Anthropic and OpenAI. Add a Google key on the API Keys tab and pick a model; the default is gemini-2.5-flash. As with the other two, the key is yours, encrypted at rest, and the server never falls back to a platform key. See Model keys.

25 August 2026

Indexing kill switch for self-hosted installsNew

INDEXING_ENABLED=false pauses document upload and indexing for everyone on that install. Embedding a whole uploaded file is the heavy operation that can pin a small box under concurrent load; search and retrieval over already-indexed documents keep working. Unset it (or set it to true) and restart to re-enable. Self-hosting only — see Self-hosting.

24 August 2026

OAuth 2.1 for MCP serversNew

Connect OAuth-protected MCP servers, not only static-token ones: authorization code with PKCE (S256), RFC 9728 protected-resource discovery, RFC 8414 authorization-server metadata, and RFC 7591 dynamic client registration, or a client id and secret you supply. Two modes — shared, where an admin connects one account for the whole project, and per-user, where each end user connects their own from the widget and the agent acts as them.

Tokens are encrypted at rest, refreshed before expiry, and pinned to the server's resource audience so a per-user token can never be forwarded to a different server. Per-user mode requires signed identity. Existing none, bearer and header servers are unchanged.

What to do: Self-hosted installs must set PUBLIC_BASE_URL. It fixes the redirect URI, which is never derived from request headers, and the OAuth endpoints refuse to run without it. See the MCP OAuth API.

23 August 2026

The scheduled-tasks button moved, and its id changedBreaking

The My-tasks button left the panel header for the footer input row, next to the attach button, and its id changed from #hd-tasks to #ft-tasks. The unread badge moved with it. A customer styling the header button lost that styling on the next page load, with no error anywhere — the rule simply stopped matching.

What to do: There is no ::part() for this button today, so #ft-tasks is the only handle and, per the contract above, it is not a stable one. If you style it, expect to revisit it. ::part(tasks) refers to the My-tasks panel, not the button that opens it.

Scheduled tasksNew

End users schedule agent tasks from chat, once or on a cron, and a single loop runs each due task as that user, through the normal agent — so budget, logs, history and vault credentials all apply. Writes need an admin-curated allowlist; without one a task is read-only. Off by default, and it requires signed identity. The widget gets a My-tasks panel, openable with ERA.openTasks(), and a banner when new results are waiting. See the Scheduling guide.

Per-user credential vaultNew

Provision long-lived per-user secrets server-to-server with PUT /v1/credentials under a signed identity. They are encrypted at rest, never returned as plaintext, and injected into webhook and MCP tool auth as {{user.creds.<name>}} — tool scope only, so the raw value never reaches the model or the logs. See Per-user credentials.

Live status line, and two fixesChanged

While the agent works, the widget shows a rotating status word and a checkmarked trail of the tools it has called, matching the console playground. Separately: enable-all / disable-all for a server's MCP tools fired one request per tool, exhausting the database connection pool and half-applying the change — it is one bulk write now. And the live-logs WebSocket rejected asymmetric (ES256) Supabase tokens, so the console log panel showed nothing on affected projects.

22 August 2026

Clickable option buttons, and MCP catalogs behind per-user authNew

The first version of the option buttons the agent can offer. Alongside it, a one-shot discovery token lets an admin import the tool catalog of an MCP server that requires per-user authentication, and asymmetric Supabase JWTs (ES256 and RS256) are verified through JWKS rather than assumed to be HS256.

Earlier

The 0.1.0 baseline: the multi-tenant platform, the provider-agnostic agent loop with bring-your-own-key, hybrid RAG with per-user document audiences, webhook tools with {{user.X}} templating and human-in-the-loop confirmation, policies, guided flows, prompt versioning, per-user memory, signed identity, and the first Shadow-DOM streaming widget. Those predate any changelog and are described in the Core concepts and the API reference rather than reconstructed here.