Deprecations and migrations
Find removed or superseded APIs and the supported migration path.
The Help panel reader moved to @k2b/cloud/ssr
Replace the former import of LayoutHelpPanel or Layout.HelpPanel:
import { LayoutHelpPanel } from "@k2b/cloud/ssr/islands";Use the server entry point instead:
import { LayoutHelpPanel, type LayoutHelpPanelProps } from "@k2b/cloud/ssr";Render it in a server page, such as inside PanelLayout, never inside an
island. The panel renders server HTML and mounts small islands for interaction.
Applications that get Help in the panel through the automatic registration
change nothing.
Confirm new personal access in a browser
Creating personal API keys and passkeys now requires a browser sign-in from
the last ten minutes. OAuth access tokens, API keys, background mandates, and
mobile app sessions receive 403 BROWSER_SIGN_IN_REQUIRED for those writes.
An older browser sign-in receives 403 REAUTHENTICATE.
Automation that created personal API keys using an OAuth token or another API
key must stop doing so. Create the key in Developer → API keys
(/me/developer) after signing in again in the browser, then provide it to the
integration through its credential store. Resource and standalone
service-account keys keep their existing rules.
The browser flow of cld login now always shows consent. New OAuth refresh families
require recent browser confirmation; terminal Cloud Login approvals must match
the exact grant. Existing refresh rotation stays unchanged. A New access
notice tells the person about each new browser-confirmed OAuth sign-in.
Deploy updated Core and OAuth together so their confirmation checks and schema agree: start Core first, then OAuth. OAuth adds the columns Core reads, so new OAuth sign-ins fail until updated OAuth has started. See OAuth clients and flows.
OAuth sign-ins end once on upgrade
Every existing OAuth sign-in of a person ends once with this upgrade. Run
cld login again and reconnect MCP clients and other applications.
Client-credentials tokens are unaffected.
Update applications built on @k2b/cloud to the released package so they enforce
the live account-epoch check. Start updated Core and OAuth together: OAuth
sign-ins and refreshes fail until both run the new release. During a rolling
update, older application versions still accept access tokens of signed-out
accounts until those tokens expire, at most one hour after issuance.
After the upgrade, signing an account out everywhere also ends its OAuth sign-ins and rejects their access tokens at the next request. See OAuth clients and flows.
Action policies and per-call reviews replace approval and catalog sentences
Action approval, CapabilityActionReview, and presentation.sentences
are deprecated and will be removed with the next major release. This release
accepts both contracts. Follow the application migration guide,
and upgrade Core and the applications that call Actions before an app returns
the new review or receipt shapes.
The new review shape is CapabilityActionCallReview with
CapabilityActionCallReviewSchema. CapabilityActionReview and
CapabilityActionReviewSchema keep the old shape in this release; the next
major release gives that name to the new shape. Because a review can now have
either shape, data in CapabilityActionReviewResult and in review client
results is CapabilityActionReviewData. Code that reads it narrows with
"sentence" in data or parses it with the schema of the shape it expects.
Cloud administrators see only granted contact books in Contacts
Cloud administrators and their user-delegated keys now need a grant to read or
change a contact book inside Contacts. This affects the Contacts app, API, app
tools and capabilities (including Mail recipient suggestions), cld contacts,
and live updates that previously let administrators read books without a grant.
Requests for a book without access return HTTP 403, forbidden, or not_found,
depending on the operation. Lists and suggestions show only granted books.
Administration → Contacts still lists every book, shows its summary, and
opens its permission dialog. The administration endpoint
/api/contacts/admin/books/:bookId/access keeps the same behavior.
To keep using a book in the app, open Administration → Contacts, open the
access dialog of the book from its row, and grant yourself access. API clients
can grant access with POST /api/contacts/admin/books/:bookId/access. Then use
the Contacts app with that grant.
Contacts requires finished short IDs
Contacts creates short_id as a required column on books, contacts, tags, and
notes. It no longer assigns missing short IDs at startup, so startup no longer
does work that grows with data.
Every Cloud release from v0.7.0 through v0.45.0 finished the short IDs on its first Contacts start. An installation that started Contacts on any of these releases needs no action. The minimum direct upgrade source for Contacts is an installation whose Contacts has started on Cloud v0.7.0 or later.
A database that never finished its short IDs, such as one upgraded straight
from the June 23, 2026 production baseline, is no longer a supported direct
upgrade source. Contacts refuses to start, points to this section, and leaves
the database unchanged. Start Contacts once on Cloud v0.45.0 until
/_cloud/ready answers ready, then upgrade to the current release.
Help articles have kinds, next steps, key caps, and details
Help frontmatter now accepts kind, needs, next, and related, and the
article body accepts key caps, icon labels, and :::details blocks. Core adds
four columns to help.documents for them. Update Core before applications that
publish Help: until Core adds the columns, an updated application that
publishes Help fails its first registration and does not start.
Startup checks Help more strictly. An application with Help that passed before fails startup when an article:
- uses
kind,needs,next, orrelatedin its frontmatter for something else; Help no longer strips these keys; - has a line starting with
:::details,:::steps,:::reference, or:::comparedirectly after a text line, a details opener without a title or closing:::, or details inside details; - has a
:kbd[...]that does not name a key; - has a callout inside details;
- has a
help:target in a reference definition, autolink, image, HTML, or callout title; - uses a Help link scheme other than lowercase
help:, such asHELP:.
Code spans and fenced code stay exempt. See In-product Help.
Package plugins ship as verified bundles
Package plugins now use the same verified manifest, bundled module, references,
and content-addressed store as plugins served by a Cloud application.
cld <module> reference [file] works for both sources. Package modules appear
with profile * in the generated agent skill table.
Packages with the old cld.apiVersion and cld.entry format are refused.
Existing plugins/<id>/ directories are listed as incompatible and never
loaded. There is no compatibility path.
Authors re-pack from their package directory and republish the output:
bun run node_modules/@k2b/cloud/scripts/cli-pack.ts src/cli.ts --references references --out dist/plugin
npm publish dist/pluginThe references directory needs index.md. The pack script writes
cld.manifest in the output's package.json.
Users install the new package with cld plugins install <source>. To remove an
old installation, run cld plugins remove <id>. See
Application CLI modules.
Mail reference number commands moved
reference is reserved as the first argument of every cld module: it prints
the module's skill references. Mail's reference number commands therefore
moved from cld mail reference … to cld mail reference-number config show,
config set, list, ensure, and find, with unchanged flags and output.
defineCliCommands() now refuses a module that declares a top-level
reference command.
@k2b/ui Calendar month reads range
The Calendar month view now reads range. With range, the month scrolls
through the weeks of that range, names the month in the middle, and calls
onLoadEarlier and onLoadLater near its ends. Before, only the day and week
views with dayHours read range, and the month always paged. A host that
passes range in every view gets the scrolling month after the upgrade. To
keep the paged month, pass range only in the day and week views, for example
range={view === "day" || view === "week" ? range : undefined}. See
Calendar.
Spaces scrolling calendars load days in blocks
The Spaces day and week views load days a week at a time. Month now scrolls
through continuous week rows and loads eight weeks at a time. The internal
GET /api/spaces/workspace/view endpoint accepts from, to, and
includeTray for cv=day, cv=week, and cv=month, for at most 366 days.
Every calendar snapshot carries range, the days it loaded from midnight
to midnight. Queries for these views include entries until 06:00 after the
last day, so timed views can keep the night with the day before. The year
view ignores from and to; an old timeline link opens the scrolling month
of its day. No setting, data migration, or operator action is required.
See Spaces.
Mail drafts keep the signature outside the body
Mail drafts now store a signature choice separately from the editable body. Preview and delivery place it after the text; when the text sits above a quoted reply or a forwarded message, the signature goes before that history. The composer offers Signature in its action bar, including No signature; switching the sender selects that sender's default. Draft copies in the provider's Drafts folder contain only the body, without the signature.
Drafts and draft seeds no longer return initialSignatureSource. Use
signatureTemplateId instead: a signature template ID, or null for none.
On create, omitting it uses the sender's default; on update or patch, omitting
it keeps the current choice. Keep signatures out of the body. Compose
suggestions return snippets only, and render-snippet refuses signatures.
Existing drafts move recognized signatures out of the body when opened or saved. Drafts that have not yet been saved again still send correctly. No configuration change is needed; Mail adds one nullable column on its next start. See Mail draft signatures for the rollout.
Studio source writes save one file per call
code_write no longer accepts a files array. Use
code_write({id, expectedRevision, path, content, entry?}) for each source file,
one call at a time, passing the returned revision as the next expectedRevision.
A stale revision still returns CONFLICT without saving. Results add lines
beside length for every file; check that the line count matches what you
wrote.
Move fromFile imports to
code_import({id, expectedRevision, files: [{path, fromFile}], entry?}).
Imports remain atomic and require one fresh review for the batch. Ordinary
source writes require no approval. Stored source and recorded chat arguments
are unchanged. Operators must update Core and Assistant together; see
Update Studio source writes.
The dashboard becomes one board of widgets in three sizes
The dashboard no longer has the focus, overview, and side zones, widget widths, arrow ordering, the name color, or the per-device cookie that left empty widgets out. Each person has one board: an ordered list of widgets, each in a small, medium, or large size, edited on the board itself and saved without a reload. A gallery lists the widgets the person may use, with live previews.
The widget declaration is extended, not replaced. defineApp({ widgets })
accepts title, description, sizes, defaultSize, suggest, and
requiresRoles, and presentation.translations.<locale>.widgets translates
titles and descriptions. A handler reads the requested size with
getWidgetRequest(c) from @k2b/cloud/server. A widget without sizes keeps
working as a large widget. presentation.defaultZone and
presentation.defaultSpan are deprecated; declare sizes and defaultSize
instead, and keep an existing presentation while saved boards are converted.
defineApp() now rejects a widget ID that contains @ and a suggest that is
not a boolean. The widget stream accepts widget=<appId>/<widgetId>@<size>,
and streamWidgets() takes sizes.
Saved dashboard settings are converted once, on each person's next visit, into
the board they saw: the saved order, the zones and widths they chose, and
otherwise their apps' recommendations. A widget in the side column becomes
small, a wide one large, and any other medium; a widget that does not offer
that size yet shows in its default size until it does. A widget switched off is
not on the board. People who never changed a widget follow the new default
board. Shortcuts stay; the name color is dropped. The
dashboard.user_settings table gets a board column; the old columns stay
and are emptied as each person's settings are converted. Deploy Core and the
Dashboard together; see
Upgrade the dashboard to one board
and Dashboard widgets.
Actions can word their own approvals and receipts
This change is additive; existing declarations behave as before. A capability
presentation catalog may now declare sentences per Action, approval,
done, rejected, and notRun, with placeholders for input and result fields,
and translate them under translations.<locale>.actions.<id>.sentences.
app.start() validates them against the Action's schemas. The Assistant's
approval card now shows what will happen as its title, with the app name in
the line below; receipts, step rows, and code approvals use the app's
sentences, and an Action without them reads as its title with up to two
labelled input fields. The approval text of cld assistant actions starts
with the app and its sentence, followed by the live review message it showed
before. "Done" receipts now read "Done: …" like the other states. The model
may add an optional approvalReason to an Action call that waits for
approval; Cloud shows it as a labelled reason and removes it before the app
sees the call. getCapabilityActionWording() in
@k2b/cloud/capabilities/server gives an app the same wording for its own
approval cards. A Cloud release that does not know sentences ignores them and
keeps the generic wording. Mail, Spaces, and Files word their main Actions. No
setting, data, or migration is involved. See
Word Actions for people.
Apps ship their own Assistant Skills
Built-in Skills such as cloud-grids, cloud-mail, and skill-creator no
longer come from templates inside Core. Each app now ships its Skills as
Markdown files and declares them with defineApp({ skills }); third-party apps
can do the same. See Ship Assistant Skills.
Skill templates are removed: seedCloudAiSkills, aiSkills.seedOnce,
aiSkills.admin.applyTemplate, the AiSkillTemplate type, the
templateId, templateVersion, currentTemplateVersion, and templateStatus
fields, GET /api/ai/skills/templates/:name,
GET /api/admin/core/ai-skills/templates,
POST /api/admin/core/ai-skills/:skillId/template, and
cld admin ai skills templates and associate. Use the new source field and
the reset, restore, and adopt actions instead. The cld admin ai skills reset
command no longer takes a template; it resets to the app version you name with
--app-version, shown by cld admin ai skills list.
On upgrade, Core links every installed built-in Skill to its app. Skill IDs,
access, personal Enabled settings, and administrator changes stay; a deleted
built-in Skill stays deleted. skill-creator now belongs to Assistant. An
unchanged Skill updates when its app starts; a changed one shows
App update available. A copy that was never linked to a template shows as
Name in use until an administrator selects Use existing Skill. App
Skills are now offered only to people who may open the app, by the same rule as
Help: guests no longer see the Skills of apps closed to them, such as Weather,
and only administrators see those of an app reached only through the admin
area. Until an app runs the new release, its Skills are not offered. The
upgrade is one-way for Core: after the new Core has started, never start an
older one against the database; restore the database backup to go back. See
Assistant Skills after the upgrade.
Mail conversations have several assignees
A Mail conversation can have up to 20 assignees, and a mailbox grant can cover only the conversations assigned to a person. The single assignee fields are gone without an alias; see Assign conversations and limit access to them.
| Before | Now |
|---|---|
assigneeUserId on list items, search hits, and focus items |
assigneeUserIds, an array in assignment order |
assigneeUserId on PATCH /api/mail/mailboxes/{mailboxId}/conversations/{conversationId}/collaboration |
assigneeUserIds, which replaces the whole set; [] clears it |
assigneeUserId on POST /api/mail/mailboxes/{mailboxId}/conversations/assign |
assigneeUserIds with mode: add, remove, or replace; replace with [] clears it |
assignee in collaboration data and in the conversation.read capability |
assignees, an array of users |
assignee in the results of the conversation.assign, conversation.snooze, and conversation.status.update capabilities |
assignees, an array of users |
assigneeUserId in the conversation.assign and conversation.assign.batch capabilities |
assigneeUserIds with mode |
expectedRevision in the conversation.assign capability |
Removed; add and remove change only the people they name |
assignee in the conversation.assign.batch result |
assignees, the users named in the request |
Workflow data field inputs.conversation.assigneeUserId |
inputs.conversation.assigneeUserIds |
cld mail assign --to replaced the assignee |
--to adds people; add --replace to replace them, use --remove to remove some |
cld mail conversation update --assignee took one user |
--assignee is repeatable or comma-separated and replaces all assignees; --unassign removes them all |
Update a third-party application or script that reads or sends the old fields
before it talks to the new Mail version. A request with assigneeUserId or,
in conversation.assign, expectedRevision fails validation, and a missing
mode fails too. These capabilities change under their existing IDs rather
than new ones, so update their callers together with Mail and refresh cached
capability schemas. Mailbox responses and the mailbox.list and
mailbox.browse capabilities gain accessScope, and access entries carry
scope: "assigned" for grants that cover only assigned conversations. Such a
grant allows replies in the assigned conversations but no new mail, so pick a
mailbox with accessScope: "mailbox" before you create new mail.
A saved workflow that refers to inputs.conversation.assigneeUserId keeps its
bound version, but the field no longer exists in its runs: a step that reads it
fails with an unavailable reference, and exists on it is false. Saving or
validating the workflow again reports a diagnostic for the unknown field. Replace it with inputs.conversation.assigneeUserIds,
for example ${{ inputs.conversation.assigneeUserIds.0 }} for the first
assignee. The workflow action assignConversation keeps its user input: it
replaces all assignees with that person, and null removes them all.
Platform administrators without a mailbox grant no longer count as mailbox users in Mail's background checks either: reminders and other conversation notifications, workflow notices, viewing presence, saved views shared with them, the person who authorized an incoming automation, and whether an assignee can still work on a conversation all need a grant, as opening the mailbox always did. An incoming automation that such an administrator authorized pauses its Spaces authorization on its next run; give them a mailbox grant with Write access and have them authorize it again. On upgrade, Mail copies each existing assignment once; see Upgrade Mail to several assignees.
Dashboard widgets stream in one by one
The dashboard no longer waits for widgets before it sends the page. It renders
a fixed space for each widget and loads all of them in the browser from the
new streamed GET /api/widgets/v1, where each widget has its own 8-second
budget instead of 500 ms and Core runs at most eight at a time. One stream ends
after 30 seconds, when its signed invocations expire, so on a dashboard with
many slow widgets the last ones can report a timeout. A widget that fails or
times out shows Try again in its own space. The single-widget
GET /api/widgets/v1/<appId>/<widgetId> keeps its responses and uses the same
8-second budget. Widget handlers and their responses are unchanged; each widget
now has the fixed height of a @k2b/ui frame, and content taller than that
scrolls inside it. The @k2b/ui compact widget frame grows from 12rem to
14rem, and fixed frames scroll instead of cutting off taller content. No
setting or migration is involved. See Dashboard widgets.
Help requires sign-in
Cloud's Help surfaces are no longer public. Before, anyone could read every
application's Help without signing in, including Help of administrator-only
applications. Now /api/help/v1/... answers 401 without sign-in, and
/help/apps/... and /app/<id>/help lead to sign-in. OAuth clients of the Help
API need the read or admin scope, as for MCP. Signed-in users only see
Help of applications they may see: an application's nav.requiresRoles
limits its Help like the navigation, and an application reached only through
the admin area shows Help to administrators. The Assistant's search_help and
read_help and the MCP Help tools and resources apply the same rule; a service
account without a user sees no Help. Weather no longer shows its link or its
Help to guests; its pages already required a full account.
Nothing needs to be configured or migrated. Links to Help from outside Cloud now ask for sign-in first. An application that renders Markdown from its own Help declaration on a public page, as API Docs does with its getting-started article, still shows that text there.
For application authors, preloadLayoutHelp(c, appId) returns null without a
signed-in user. If guests may not use your application, declare
nav.requiresRoles: ["user"] so guests see neither its link nor its Help.
defineHelpCollection() is deprecated: its router now requires sign-in but does
not apply the Help visibility rule, so only the mounting application's routes
guard it. Declare Help with defineHelp() and pass it to app.start({ help })
instead. See Who can read Help.
Spaces removes the calendar timeline
The Spaces calendar no longer has a Timeline view. A saved link with
cv=timeline opens the month view of its day instead of failing. The row of
overdue tasks and the reader's undated tasks moves below the Day view.
The internal GET /api/spaces/workspace/view endpoint no longer reads from,
to, and includeTray, and a calendar snapshot no longer carries range;
only the timeline used them. A day view snapshot carries tray. No setting,
data, or migration is involved. See Spaces.
Applications can search their sent mail and read related apps' mail
This change is additive; existing calls behave as before. mail.list gains
the filters q (a case-insensitive substring of a recipient or the subject),
recipient (one exact address), until, and apps, and every MailRecord
now carries appId. Without apps, an application still reads only its own
mail. An application that declares the new platform permission mail:read
can read the mail of the applications an administrator chooses, through
mail.readableApps() and filter.apps, without their message text.
Applications that only declare mail:read can also call mail.list.
Operators decide in Administration → Outgoing mail → Apps or with
cld admin outgoing-mail apps set-log-access; nothing is shared until then.
On upgrade, Core adds a grant table and builds two search indexes on the send
log in the background, without delaying its start or new mail. Until they are
built, a search on a large log can stop after five seconds with bad_input. See
Read other apps' mail and
Let an app read other apps' mail.
Assistant notices when an offer fits
At the start of a followed turn, Cloud now checks the message and the latest messages of the chat for three offer cases: a second format correction in a row, a tone correction of a mail draft, and a reference to earlier work such as "like last week". When one fits, the turn gets one short instruction to offer a Skill or to remember the preference once at the end, so models that skip the general Suggestions rules still make the offer. The tone case no longer requires a Skill to have shaped the result. A question such as "What can you do for me?" adds a bounded summary of the user's other chats and of the Cloud items used in them; it holds only Assistant data, not which apps have data for the user. Suggestion limits, organization instructions, and a user's request for no suggestions still take precedence, and the instruction repeats them. No setting changes; scheduled task runs are unaffected. See Turn recurring work into a Skill.
Security fix: the gateway keeps requests on the application's host
This release contains a security fix in the gateway. Update promptly.
Before this release, a request whose path started with two slashes could make
the gateway forward it, with its cookies and other headers, to a host other
than the matched application. The gateway now always sends a request to the
host and port of the application's baseUrl, for HTTP and WebSocket, and
passes a leading run of slashes on as one slash. Other paths, including inner
double slashes and percent-encoded characters, reach the application
unchanged.
Whether an installation was exposed depends on what sits in front of the
gateway. compose.prod.yml runs behind the operator's external Traefik and
does not pin its version:
- Traefik v2.11.24 or a later v2.11 release, or v3.3.6 or later, with the
default
sanitizePath: truecollapses duplicate slashes before the gateway sees the request. An installation behind it was not exposed this way. - An older Traefik, including v3.0 to v3.3.5, an entrypoint with
sanitizePath: false, another reverse proxy that passes the path unchanged, or a gateway port that clients reach directly forwarded such requests. Update, then check the reverse proxy's access logs for request paths that start with//. If such requests came from untrusted sources, treat the sessions of the users who sent them as exposed and revoke them; see Emergency signing-key revocation for invalidating every session at once.
The same release also hardens related path handling:
- Sign-in redirects and the configured home path no longer accept a target
whose dot segments resolve to
//host. - Command paths and
returnTodestinations, including the Mail composer's return address, no longer accept a path whose dot segments resolve to//host. - Proxy authentication returns to the protected host's root when
X-Forwarded-Uriwould resolve to another host or does not parse, and ignores anX-Forwarded-Hostvalue that is not a plain host. - The Jobs Run now action in Gateway operations and the Grids last-visited base stay within Cloud for the same kind of path.
No configuration change is needed.
A failing island shows a notice instead of freezing
Cloud now uses @k2b/ssr 0.15 and @k2b/stdlib 0.28. Each island mounts
inside its own error boundary, and defineApp gives it Cloud's localized
notice with Try again. See
When an island fails.
Remove root ErrorBoundary wrappers that an island only had for protection.
Dialogs and floating windows from @k2b/ui get their own boundary and notice.
A dialog whose content throws while it opens no longer closes and rejects its
promise; it shows the notice and resolves undefined once closed.
Query and mutation state now describes only the load or the write. A component
that throws while it renders a query's or mutation's data no longer sets
error() or calls onError. onSuccess runs after data() and loading()
are updated, and mutate() and retry() reject with an error thrown by
onSuccess. Code that relied on onError for such errors moves that handling
to try/catch around await mutate(). See
Browser clients and mutations.
A Link from @k2b/ssr/nav without onNavigate is now a plain link with a
full page load; replace and scroll apply only together with onNavigate.
The @k2b/ui navigation components already pass onNavigate when they enhance
navigation.
AI provider behavior with Nessi 0.17
Review AI model output limits and budgets when upgrading:
- Anthropic defaults to 8,192 output tokens, including thinking, unless the profile or call sets an explicit limit. Budget reservations use that default.
- Gemini output usage includes thinking tokens, so reported reference costs and budget consumption can exceed visible-answer token counts.
- Failed and aborted turns retain already-reported usage. Each provider request is charged once, and its loop aggregate includes that usage for display.
- Provider-stopped answers, including refusals and content filters, end as failed turns. Their tool calls never run or resume, and the chat offers Continue.
- Structured tasks and compaction keep their previous reasoning requests; only
OpenRouter now receives that
lowlevel in itsreasoningobject instead of a top-levelreasoning_effort. - An Ollama profile with
contextWindownow sends it asnum_ctx, so Ollama allocates that window instead of its memory-dependent default. Check that the server has memory for it, or clear the field.
See Models and providers, Failed turns, and Usage and feedback.
Applications send mail through outgoing mail
Application authors replace app-owned SMTP settings and transports with mail
from @k2b/cloud/services and declare platformPermissions: ["mail:send"].
Operators can reuse the application's former SMTP account as a sender profile
and assign it to that application before removing its old deployment secret.
Follow the application migration guide
and operator migration steps.
Assistant keeps calculate loaded and offers after corrections
calculate is no longer a deferred built-in: a turn that offers it can call
it without load_tools, and only then does the platform prompt add a rule that
requires it for every amount, sum, or other number the model derives itself.
Numbers that a tool, file, or code result returns are stated as returned. A
fixed conversation tool scope that excludes calculate, or a model without
tool support, gets neither. The prompt also shows dates and times in the
runtime time zone, sends references to earlier work outside the current chat
to core.ai.chats.search, and lists the Skill and preference offer cases
under Suggestions instead of the Skill catalog.
The built-in cloud-assistant Skill template moves to version 3 with a
description that names such references, and assistant-code-mode moves to
version 62 with a description that points plain arithmetic to calculate. An
unmodified copy that is already linked to its template updates at the next
start; a customized one shows Update available under Admin > AI >
Skills. An older copy without a template link stays unchanged until an
administrator selects Link to template, as described in
Update and override app Skills.
No setting changes. See
Turn recurring work into a Skill.
Notebooks levels keep title order until arranged by hand
Writers can now arrange the notes of a level in Notebooks; the sidebar, Book,
export, and cld notebooks ls and tree follow that order. A level whose notes
all have position 0 reads by title, and arranging one note numbers the whole
level from 1. See Notebooks.
The first start of this release sets every existing position to 0 once and
records that in notebooks.data_migrations. Earlier releases stored the
creation order there, which only the notebook export used; exports of existing
notebooks now list notes by title until someone arranges a level. Export
compares titles in one fixed English collation, so every export of a notebook
lists it in the same order, whoever downloads it. Templates no longer store
their note order.
In the Notebooks API (/api/notebooks/{id}/notes, PATCH …/notes/{noteId},
POST …/notes/{noteId}/move) and the note.create and note.move
capabilities, position is now the 0-based place among the siblings and
renumbers that level; a value past the end appends. A move that only changes
parentId no longer needs position: the note joins the end of an arranged
level or the title order. move and note.move also accept before or
after, and move accepts position: "first" | "last"; note.move keeps its
integer position. POST /api/notebooks/{id}/note-order/reset
with { "parentId": … } sets a level back to title order. cld notebooks mv
no longer sends the old position with every move.
While replicas of the previous release still run, notes they create take the old creation-order position. That arranges an alphabetical level in creation order, and in a level arranged by hand such a note can share its number with another note, so the two read by title. Sort the level alphabetically from the folder menu, or move any of its notes up or down, which numbers the level again.
Notebooks CLI plugins and browser tabs from before the upgrade still send a
note's old position with every move, so moving a note to another folder puts it
at that place, usually first, and arranges that folder by hand. Update the
plugin with cld plugins update notebooks and reload open Notebooks tabs; sort
an affected folder alphabetically from its menu.
Assistant may end a reply with one offer
The platform prompt now tells the model what the chat keeps visible and allows
one concrete offer at the end of a reply, such as drafting an answer or saving
recurring work as a Skill. It replaces the earlier rule against repeated offers.
Scheduled task runs do not get these sections. Organization instructions that
ask for no suggestions, or for a different closing, take precedence; review
existing organization instructions that mention offers or follow-up questions.
The model also keeps intermediate files below the chat folder /temp/. See
Files, Projects, Skills, and personalization.
Failed Assistant turns say why and how to go on
A failed Assistant turn now stays visible in the chat with a notice that says
why it failed and what comes next, and offers Continue when a new turn can
finish the work. The server records the reason as a stable code in
meta.turnError on the turn's last message; see
Failed turns.
AiTurn.error, AiConversation.runError, and the error of turn_finished
no longer carry the provider's own message or other internal text, but the
worded reason, for example "The model service did not answer. The results so
far are kept. Send a new message to continue." Code that matched those texts
reads meta.turnError.code instead. Operators find the raw cause in the error
log entry AI turn failed under ai:executor. A failed turn no longer streams
a temporary "⚠️" text block. The chat controller's error() no longer repeats
a failure the turn's notice shows. AiConversation.runTurnId names the latest
turn, which runStatus and runError describe.
Applications that host the chat timeline add
AiChatActions.onContinueTurn(message) and send message through their
normal send path to offer Continue; without it, the notice shows the
reason only. See Failed turns.
Turns that failed before the upgrade keep "Worked 4 min · interrupted" and show no notice. A chat whose earlier turn left a call without a result, such as a stop while an approval waited, no longer fails its next turn at providers that reject such a history. No setting changes. Core and Assistant can update in either order; Assistant shows Continue once both run this release.
Studio apps are plain HTML
This breaking Assistant change replaces the ui tree with HTML apps. An app's
interface is now index.html, optional style.css and app.js, and further
JavaScript modules, running in a locked frame with the same cloud library as
scripts. See Assistant. Existing app
source is not migrated: an app without index.html has no interface until it is
rebuilt, for example with Edit, and its published actions keep working.
| Previous | Replacement |
|---|---|
ui.* controls, layouts, Chart Explorer, ui.modal.* |
HTML elements styled by the base stylesheet; <dialog> for questions; cloud.chart for charts |
ui.filePicker |
<input type="file"> |
| A GUI entry that default-exports a function | index.html; code_create starts with one |
code_interact |
removed; the name is ignored in clientToolIds; scripts are tested with code_run, HTML apps with code_check |
code_inspect with nodeId, offset, limit |
code_inspect({runId, waitMs?}) |
code_present({runId, title}) |
code_present({title, files}) for a one-off app, code_present({id}) for a saved app |
code_run({id}) on an app with a GUI |
code_open or code_present; code_run refuses an index.html entry |
Runner route GET /api/assistant/runner/:id/compiled |
GET /api/assistant/runner/:id/app returns published files and the viewer context |
| Public links to apps | switched off for now; see Assistant |
Chat presentations saved as UI-tree snapshots cannot be shown anymore. The
Assistant startup migration clears them once and stores new presentations as
app files or a reference to a saved app; the chat_presentation_inputs table is
removed. Operators deploy the app frame prelude and base stylesheet with the
Assistant image; see Build and deploy.
cloud.chart returns the @k2b/ui chart markup (.k2b-chart with
.k2b-chart__svg) instead of .cloud-chart, and cloud.pdf.render styles PDFs
with the same base stylesheet as apps.
Assistant offers Skills for recurring work
In chats a person follows, Assistant now recognizes recurring work and may
offer once to save the approach as a personal Skill; the Skill draft keeps
procedure and format and leaves out chat content, names, amounts, and resource
IDs other than a Studio App the Skill calls. The offer appears only while
skill-creator is enabled for the user. The built-in skill-creator template
moves to version 3. An unmodified installation updates at the next start. A
customized copy keeps its content and shows Update available under
Admin > AI > Skills until an administrator resets it. No setting changes.
Scheduled task runs are unaffected. See
Turn recurring work into a Skill.
Assistant offers code tools in every turn and says why a tool is missing
Every Assistant turn with the default tool source now offers the server-run code
tools code_run, code_action, code_inspect, code_stop, code_export,
and code_present. Before, only turns whose client listed them
in clientToolIds had them, so cld assistant --detach, API clients, and
scheduled tasks could not run code. Scheduled tasks now run them with their
task grants, as Scheduled Code Mode
describes. code_open, code_secret, and the local client tools
(local_bash, local_list, local_read, local_upload, local_download,
local_write, local_move, load_local_skill) need a client that declares
and handles them. Clients that list the server-run names keep working; the
names have no effect.
The load_tools result changed. loaded and alreadyLoaded hold
{name, call} objects instead of names, and missing is replaced by
unavailable, a list of {name, reason} with the reasons unknown,
not_offered_in_turn, not_allowed, and app_offline. Code that reads
load_tools tool blocks from the conversation API reads the new fields; the
chat still shows results stored in the old shape. load_tools also accepts the
provider name of an app operation, and a call to a loaded app operation by its
capability ID now runs instead of failing as an unknown tool. See
Tools and approvals.
No setting changes.
Studio script and action library
This breaking Assistant runtime change replaces top-level helpers with one
frozen cloud global. Existing app source is not automatically migrated.
Update scripts and actions before deploying the new runtime. Operators should
ship the eager worker, content-hashed lazy chunks, and chunk manifest together.
Assistant’s normal startup migration adds personal KV storage; existing database
tables gain nullable audit columns on first access, without backfill.
| Previous API | Replacement |
|---|---|
| ai.generateText / classify / classifyMany / extractData | cloud.ai.text / classify (multiple) / extract |
| money | cloud.money; format/parse default to viewer locale |
| datev / sepa / camt / einvoice | cloud.finance.*; await every method |
| ids.ulid() | crypto.randomUUID() |
| http.fetch / secret | cloud.http.fetch / cloud.http.secret; every request requires approval |
| capabilities.run / streams | cloud.capabilities.run / streams |
| database.connect().table(name) | flat cloud.db.list/get/insert/update/delete; schema uses code_database tools |
| kv.shared | cloud.kv |
| kv.local | removed; use server-side cloud.kv.user for personal JSON |
| files.shared | cloud.files |
| files.local / store / opfs | removed; no personal file store or browser-local runtime storage |
| files.list/read for run inputs | script context files with path/size/type/file() |
| files.open/openMultiple/openFolder/path | removed; HTML apps use <input type="file"> |
| files.save(data,name) | cloud.download(name,data) |
| sheet.fromCsv / openExcel / openOds | cloud.sheet.parseCsv / read (detected from bytes) |
| sheet.toCsv | await cloud.sheet.toCsv before downloading |
| pdf.open / facturX | cloud.pdf.read / render({facturX}) |
| work.* | script context signal/progress |
| ui.* | removed; see Studio apps are plain HTML |
Lists return at most 1,000 rows and raise limit when paging is needed. The
server sets created_by/updated_by and enforces each table’s everyone/own/managers
write rule. Personal KV is isolated per app and viewer across devices; anonymous
public-share visitors cannot use it or write database rows. Cloud calls fail with
CloudError and one of the documented stable codes. Secret-bearing HTTP responses
redact raw, prefixed, base64/base64url, JSON-escaped (including escaped slashes and ASCII Unicode escapes), and URL-encoded secret forms before reaching code.
Assistant turns end loops and long runs with an answer
A chat turn, including scheduled and background ones, now stops using tools and answers in three cases where it used to go on until it failed:
- In the last tenth of the run time limit (
ai.turn_timeout_minutes, default 30), so with the default after 27 minutes. A turn that used to fail with "Run time limit reached" now usually completes with what it has done and what is still open. - When the same tool call fails twice with the same input, or after six tool searches in a row without a completed step, the model first gets a hint; when either pattern appears again, the turn answers without tools. A steering message gives the turn its tools back.
No setting changes. Operators who want turns to keep their full run time for
tools raise ai.turn_timeout_minutes. See
Loops within a turn.
Code tool failures are tool errors, and App action inputs are objects
Assistant's server-run code tools now report a call that does not complete as a
tool error, and the chat shows that step as failed. The code host returns such a
call as CodeToolFailure ({failed: true, error, guidance?}, exported from
@k2b/cloud/ai/browser). The earlier {error} results and the kind: "input",
kind: "host", and retryable fields are gone. A run whose code fails still
returns its snapshot with status: "error". cld assistant code run and
cld assistant code action fail as before when a started call does not
complete; the error they report is now the CodeToolFailure object.
code_action takes its input as a JSON object and defaults to {}. JSON
text, numbers, arrays, and null are rejected, and a value that does not match
the action's schema returns ACTION_INPUT_INVALID naming each rejected field.
Publication rejects an action whose inputSchema does not have
"type": "object"; saved revisions keep compiling.
Studio Apps published earlier can still list an action with a scalar or array
inputSchema. That action can no longer be called: its author must change the
schema to an object, read the value from that object in the handler, and
publish again. To list the current publications whose action input is not an
object schema, run:
SELECT artifact.short_id, artifact.published_title, action->>'name' AS action
FROM assistant.artifacts artifact
JOIN assistant.artifact_revisions revision
ON revision.artifact_id = artifact.id AND revision.revision = artifact.published_revision
CROSS JOIN LATERAL jsonb_array_elements(revision.source->'files') AS file
CROSS JOIN LATERAL jsonb_array_elements((file->>'content')::jsonb->'actions') AS action
WHERE file->>'path' = 'app.actions.json'
AND action->'inputSchema'->>'type' IS DISTINCT FROM 'object';An action without a type in its input schema stays callable with an object,
but its App cannot be published again until the schema says "type": "object".
Update Core and Assistant together. While their versions differ, and when a turn resumes a call that an earlier release completed, a failed call can still reach the agent as a successful result, as in earlier releases.
Assistant turns fold finished work into one line
The chat shows each Assistant turn in four places: one work line, the delivered results, the newest text, and receipts for actions and decisions. See Chat interface.
AiChatActions.renderCodePresentation now receives an accessor instead of a
value. Cloud calls it once per code_present call, as soon as the call starts,
so the host can reserve its frame and keep a running session when the turn
ends. Replace renderCodePresentation: (result) => <View result={result} />
with renderCodePresentation: (result) => <View result={result()} />, and
treat undefined as "still running".
The server records two facts it did not record before:
- A turn that ends without a loop end of its own, such as a stop while an
approval waits or a turn the sweep finalizes, stores
abortedafter a stop, orerrorwhen it failed or its wait expired, as the loop end of its last assistant message. Turns that ended this way before the upgrade show as finished. - The stored result of a call the user approved carries the decision, so the approval stays visible as a receipt; a call that never returned keeps the decision on the message that holds it. Approvals decided before the upgrade show as ordinary steps, except Cloud actions, which always get a receipt.
- The time a secret prompt in Studio waits for its answer now counts as waiting instead of work, as approvals and survey answers do.
The live turn snapshot carries actionWaitMs and waitingSince, so a client
that reconnects shows the same work time. Both fields are optional: Core and
Assistant can update in either order, and open Assistant tabs show the new view
after a reload.
Conversation streams announce provider retries
The conversation stream now sends a provider_retry event while a model call
waits to be retried after a transient provider failure; see
Transient provider failures.
Only turn_finished ends a turn. A client must ignore event types it does not
know, as the chat controller of earlier @k2b/cloud releases already does.
The assistant CLI plugin of earlier releases reads any event it does not know
as the end of the turn. At the first retry, cld assistant and
cld assistant turns watch stop following the turn and exit with status 0,
while the turn keeps running on the server; --json and --jsonl report a
finished turn without status and with empty text. A profile keeps its plugin
version until you update it, and cld update replaces only the cld binary.
Update the plugin of each profile:
cld plugins update assistant # the current profile
cld plugins update --all # every profileAssistant learns from private chats by default
Learning personalization from private chats is now on for everyone who has not
chosen. Earlier releases kept it off until a person turned it on. A person's
own choice always wins: Cloud stores a choice only when they save the Learn
personalization from private chats switch in Assistant settings >
Personalization or run cld assistant personalization configure --learning,
and they can turn learning off at any time. Saving Use personalization alone
no longer records a learning choice. Learning needs AI and a usable background
model, reads only private chats, and stays within the monthly budget described
in
Personalization.
Learning now considers only private-chat turns that finish while it is on. When Core first starts this release, it marks every earlier turn and workflow receipt of people who had not turned learning on as already considered, so the new default never processes chats from before the upgrade. Later, chats that finish while a person has learning off are never learned from either, also not after they turn it on.
Earlier releases stored "off" for every person who ran an Assistant turn, so a stored "off" can be an explicit choice or just the old default. Cloud cannot tell them apart and keeps all of them off. Only people without a stored choice, in practice those who have not used Assistant yet, follow the new default. To see how many people keep an "off" from an earlier release, run:
SELECT count(*) FROM ai.user_prefs WHERE memory_learning_enabled = FALSE AND memory_learning_chosen_at IS NULL;Switch earlier "off" values to the default
Learning is private-data processing, so tell people before it starts, for example in your release announcement: it is on unless they turn it off, it saves facts, preferences, and repeated workflows only from their own private chats that finish from now on, and they can review or delete every entry under Saved personalization. If you then decide that the "off" values from earlier releases should follow the new default, run:
UPDATE ai.user_prefs SET memory_learning_enabled = NULL
WHERE memory_learning_enabled = FALSE
AND memory_learning_chosen_at IS NULL
AND NOT EXISTS (SELECT 1 FROM ai.memory_learning_runs run WHERE run.user_id = ai.user_prefs.user_id);The statement never changes a choice saved since the upgrade, so you can run it at any time. It also leaves out people for whom learning has already run at least once: they had learning on, so their "off" is certainly their own choice. Run history proves nothing about anyone else. An "off" from an earlier release can still be deliberate, for example when someone turned learning on and off again before it ever ran, and the statement switches those people on as well. Learning then starts with chats that finish after the statement; their earlier chats stay unprocessed.
There is no installation-wide setting for the default; learning stops only while AI is disabled or no background model is available.
Upgrade and roll back
Update Core and Assistant together and keep the mixed state short. A Core replica of an earlier release shows learning as off for people without a stored choice, although updated replicas already learn for them, and the chats it finishes for people with learning off are not marked, so those chats can still be learned from if learning is turned on for them later. An Assistant of an earlier release saves both personalization switches together, so saving Use personalization there also stores the learning switch as a choice.
Earlier releases treat a missing choice as off, but their settings dialog sends the missing choice back, so saving only Use personalization fails for those people. Before you roll back, store the earlier default for them and restore the earlier column definition:
UPDATE ai.user_prefs SET memory_learning_enabled = FALSE WHERE memory_learning_enabled IS NULL;
ALTER TABLE ai.user_prefs
ALTER COLUMN memory_learning_enabled SET DEFAULT FALSE,
ALTER COLUMN memory_learning_enabled SET NOT NULL;After a later upgrade, these people count as an "off" from an earlier release again, and so does every "off" saved while the earlier release ran. Chats that the earlier release finished can be learned from once learning is on for their owner.
Resources keep at least one manager
Grids Bases, Pulse Bases, Venues, and contact books now refuse a grant change
that would leave them without a manager. They answer 409 with code
LAST_MANAGER; Venue used to answer a removal of its last admin with 400,
and Contacts answered both a removal and a lowering of the last admin with
400. API keys bound to the resource and user-delegated accounts do not count
as managers. Grids also refuses a None grant for the same person, group, or
account as the last manager.
The administration routes follow the same rule; an administrator recovers a
resource by granting a new manager first. Pulse, Venue, and Contacts now also
check under the same lock that the person who makes the change still manages
the resource.
Spaces, mailboxes, notebooks, Assistant projects and skills, and Studio
artifacts keep their own older checks for now. They also refuse to
remove the last admin, but they count every admin entry, including API keys
bound to the resource, and answer with their own status and message, not
LAST_MANAGER.
PermissionEditor locks the row of the only manager in every application,
including third-party applications on this release. Add the server check with
ensureManagerRemains() as described in
Keep at least one manager.
The editor counts only the entries it shows. If your editor hides every
service-account entry, as the API-key guide used to recommend, hide only the
resource-bound ones; otherwise a person who manages next to an agent is locked.
The access settings of contact books, Spaces, notebooks, and Venues now show
every grant except the resource's own API keys, which stay in the API key list.
Grids access lists now name people by their display name and show their photo.
The displayName of a person's entry in the Grids API and CLI is their
display name instead of their login name; match principals by ID, not by name.
Info blocks follow one grammar
Markdown info blocks (:::note, :::info, :::success, :::warning, and
:::danger) follow one grammar in MarkdownView, the shared
markdown.render() helpers, Help, Notebooks, and PDF exports. Stored content
that relied on the older rules of one renderer can look different:
| Before | Now |
|---|---|
| The shared renderer drew a block inside a list item | Blocks work at the top level only; inside a list, a quote, or another block the text stays as written |
A line that starts with :::, such as :::trailing, closed a block in the shared renderer |
Only a line that holds just ::: closes a block |
| The shared renderer read only bold, emphasis, and inline code in the body | The body is ordinary Markdown, including lists, links, and code |
| A block right after a paragraph line stayed paragraph text in the shared renderer | It starts a block, as in Notebooks |
Notebooks showed :::warning Before deleting and :::Warning as plain text |
Both render: the title is the visible heading, and the type takes any letter case |
Move a block out of a list, or end it with a bare ::: line, to keep it
rendering. See Markdown content for the
exact rules.
Applications can offer files to other applications
Capability manifests can now declare an optional fileProvider; see
File providers. A manifest without it keeps
its earlier shape and hash, so nothing changes for applications that do not
offer files.
Files declares itself as a provider, and apps add files from it through the
shared file chooser, starting with attachments in Mail. Update Core before
Files: a Core of cloud-v0.29.0 or earlier drops Files from its catalog once
Files declares fileProvider. Catalog readers of cloud-v0.29.0 or earlier
fail on the whole catalog page that carries the Files entry until they are
updated once: third-party applications on an earlier @k2b/cloud, whose global
search and listCapabilityCatalog() read the catalog, and a capabilities
plugin of a cld profile that was not updated
(cld plugins update capabilities). Built-in applications of the same release
are already tolerant.
Before one of your own applications declares fileProvider, update Core,
your other applications on @k2b/cloud, and the capabilities plugin of each
cld profile to this release, as described in
Capability readers ignore fields from newer releases.
Readers of earlier releases cannot read a manifest that carries fileProvider:
Core drops the application, and the other readers fail on the whole catalog
page. See Roll out a provider.
Capability readers ignore fields from newer releases
Capability manifests and catalog pages are now read tolerantly: a reader ignores top-level fields that a newer Cloud release added and leaves out only the entries it cannot read completely. Readers of earlier releases fail on a whole catalog page, or skip the application, as soon as its manifest carries one new field. See Read manifests from other Cloud releases.
- Update
@k2b/cloudonce in your own applications. Their global search reads the catalog for Commands, as dolistCapabilityCatalog(),getCapabilityCatalogApp(), andCapabilityCatalogSchema. Later manifest fields then no longer break them. - Update the
capabilitiesCLI plugin of eachcldprofile withcld plugins update capabilities. app.start()now rejects a capability declaration field its release does not define, at every level, instead of ignoring it: in the declaration, a Type, Query, Action, or Command, Universal Search, a search tag, or a stream. Remove such a field or update@k2b/cloudto the release that defines it.CapabilityManifestSchemais now a Zod pipe instead of an object schema. It no longer has.shape,.extend(),.pick(), or.strict(), andz.input<typeof CapabilityManifestSchema>isunknown. Parse with it and use theCapabilityManifesttype for the result.
Files references survive rename and move
On roots where Filegate 7 reports stable file IDs (index: true and
managed: true), Files now hands out filesv2.entry refs of the form
n:<baseId>:<fileId>. Spaces links, Grids resource fields, Assistant App data,
copied references, and cld filesv2 file IDs created from now on keep naming
the same file or folder after a rename or move. Roots without stable IDs,
typically FreeIPA storage that users also write over NFS or SSH, keep path
refs. Nothing needs to be migrated:
- Refs stored earlier keep their path meaning and keep resolving. Files does not rewrite them, so the same file can have an older path ref and a newer stable ref.
- Check Stable file IDs for each root in
/admin/filesv2. If it is off where you expect it, check that the root hasindex: trueandmanaged: trueand that Filegate is 7.0 or later. - A backup restore onto new inodes or another device, or turning
managedoff, makes the stable refs of that rootnot_found. Back up as described in Files supports Filegate 7. - Update the Files CLI plugin of each profile with
cld plugins update filesv2. An older plugin readsn:…as an area namednand rejects it.
If you roll Files back to an earlier release, nothing is rewritten. Refs
stored before this update keep working, and so do path refs handed out after
it. An earlier Files cannot read stable n:… refs: wherever one is stored, in
a Spaces link, a Grids resource field, Assistant App data, a copied reference,
or a cld filesv2 file ID, it answers not_found until Files runs this
release or a later one again.
See Refer to files and folders.
Assistant live updates use the shared live layer
Core now serves AI live updates on the shared live layer, at the same path
/api/ai/live, with one channel, user. Database triggers on the AI tables
write them to the platform outbox in the transaction of each change; who
receives a Project change is still decided when it is written. The visible
conversation streams over SSE in the browser too, from
/api/ai/conversations/:id/stream, as in the CLI. A returning tab replays the
updates it missed instead of reloading every Assistant view. See
AI live updates.
The previous AI live protocol, its table, and its dispatcher are removed. Code that used these exports stops type-checking:
| Removed | Use instead |
|---|---|
aiLiveRoutes, AiLiveRoutes, and AiLiveRoutesConfig from @k2b/cloud/ai/live |
Nothing: Core mounts the AI live socket |
latestAiInvalidationCursor(userId) from @k2b/cloud/ai/live |
aiLive.cursor() from @k2b/cloud/ai/live |
AI_LIVE_WS_TYPE, AiLiveClientMessage, AiLiveClientMessageSchema, AiLiveServerMessage, AiLiveServerMessageSchema, AiLiveCursorSchema, AiLiveRevocationCode, AiLiveErrorCode, AiTurnErrorCode, AiStreamEventSchema, aiTurnEventMessage, and parseAiLiveServerMessage from @k2b/cloud/ai/live-events and @k2b/cloud/ai |
liveConnection() from @k2b/cloud/browser/live speaks the socket protocol; parse the data with AiInvalidationSchema |
createAiLiveConnection, AiLiveConnection, and CreateAiLiveConnectionOptions from @k2b/cloud/ai/solid |
createAiChatController() with its default SSE stream, plus liveConnection("/api/ai/live").subscribe("user", {}, …) |
createPgOutbox() options onDelivered and maxAttempts, and its unordered mode without orderBy and sequence, from @k2b/cloud/services/outbox |
orderBy and sequence are required; a published row is deleted, and a failing row is retried until it is published |
AI_INVALIDATION_DOMAINS, AiInvalidationSchema, createAiChatController(),
AiConversationStreamTransport, parseAiSse(), and the conversation SSE
route stay. The data of an AI live update is the AiInvalidation it was
before.
Update Core first
- Replace every Core replica with this release, and make sure no Core replica
of an earlier release starts again afterwards. Core's migration replaces
ai.enqueue_live_for_user(); an earlier Core that starts again restores its own version, and its updates then reach no current Assistant tab. - Then update Assistant and Gateway Ops.
Assistant tabs that are open during the update lose their live socket once and show "Live access changed or expired." until they are reloaded. Turns keep running on the server; a frontend tool or approval that waits for such a tab waits until it is reloaded. A tab of the new Assistant that reaches an earlier Core reloads once and then shows that live updates stopped.
Gateway health reports an error on Core while ai.enqueue_live_for_user()
does not write to events.outbox: "AI live updates are not published: an
older Core restored ai.enqueue_live_for_user". Stop every older Core replica,
then restart one current Core replica; its migration restores the function.
Gateway Ops of this release also reports this error while Core's migration
has not run yet.
Proxies in front of Cloud must not buffer text/event-stream responses,
because browsers now stream conversations over SSE.
Roll back
Roll back Core, Assistant, and Gateway Ops together, as the whole release.
Gateway Ops of this release would report the function that the earlier Core
restores as the error above and ask you to stop that Core. The earlier Core
restores its table and function when it starts. AI live updates that this
release wrote but had not published yet stay in events.outbox; they are
hints only, and you can delete them:
DELETE FROM events.outbox WHERE kind = 'live' AND app_id = 'core';Removed in a later release
Nothing writes ai.live_invalidation_outbox or the topic
cloud-ai-invalidations anymore. Until a later release removes both, an
earlier Core that still runs during the update finds an empty table instead of
an error, and the topic keeps its 128 MiB reservation; see
Deployment requirements.
Notices have one calm look
NoticeCard from @k2b/ui has one appearance: a light tint of its tone, no
icon, and neutral text. Its API and appearance stay unchanged. Markdown
callouts now show a type icon and a title in the tone colour. Code that used
the removed API stops type-checking; update it as follows.
| Old | Current |
|---|---|
NoticeCard without tone, shown as a warning |
Set tone="warning" where a warning is meant; without tone a notice is now neutral |
NoticeCard icon |
Removed. Name the state in title instead |
NOTICE_CARD_ICONS |
Removed |
NOTICE_CARD_CLASSES.inner and .icon |
Removed. Put the title and body directly in the root |
markdown.render() and markdown.renderSync() option { notices } |
Removed. Every Markdown callout shows a type icon and tone |
Markdown callouts show a decorative type icon and a tint, with the icon and
title in the tone colour. Body text stays neutral.
An explicit :::warning Before deleting title stays the visible heading. An
untitled callout names its type for screen readers only; sighted readers see
the icon and tint. Add a title where the type matters. See Notices and
Use callouts.
Grids writes live updates through the platform outbox
Grids writes its live updates through the platform outbox that Core's
migration creates, in the transaction that makes a change, so a committed
change can no longer miss open pages. Update Core before Grids: Grids does not
start until events.outbox exists. See
Live updates.
Grids pages share one socket on /api/grids/live with three channels:
records follows the records of one table, metadata the structure and
access of one Base, and runs the runs of one workflow. The socket reads the
topic cloud:live:grids and checks, when it delivers an update, that the
reader may read the Base, by the same rules as the Grids API. A changed grant
and a deleted Base check the readers of the Base's tables, structure, and runs
at once. The socket accepts every credential the Grids API accepts, so pages
in the phone app receive live updates too. A returning tab receives what it
missed instead of checking the Base structure again; the check every 30
seconds is gone.
grids.enqueue_record_event() keeps its signature. Besides the record event
that feeds record-event workflows and the change feed (/changes), it now
writes the live update of the table, and of every Combined table that shows a
changed field. The change feed, record-event triggers, and their workflow queue
are unchanged. A run update is written right after the workflow runtime has
stored the transition; one above 32 KiB makes open run details read the run
again.
A workflow action in a published Grids app no longer holds its status request open until the run changes. The page asks for the status again after half a second, then less often, at most every two seconds, as it already did when live updates were unavailable.
The first Grids start after the update replaces
grids.enqueue_record_event(). Replicas of the previous version that still
run during a rolling update then write the live updates of record changes as
well, and the new replicas publish them; structure and run updates of an old
replica reach only tabs connected to it. A replica of the previous version
that starts during the rollout puts the previous function back, so new tabs
miss record updates until a new replica starts again. For one release,
/api/grids/ws closes the socket of a tab opened before the update with
reload_required; the tab shows that live updates stopped and offers a
reload.
Nothing writes the previous topics grids:records, grids:metadata, and
grids:workflow-runs anymore. Their streams keep their reservations of 1 GiB,
128 MiB, and 512 MiB until the next release removes them together with
/api/grids/ws; see
Deployment requirements.
If you roll Grids back, its start restores the previous function and live
updates work as before. Updates the new version wrote but did not publish yet
wait in events.outbox until Grids is updated again.
Mail writes live updates through the platform outbox
Mail writes its live updates through the platform outbox that Core's migration
creates, from the trigger on its activity log, so they are part of the
transaction that records a change. Update Core before Mail: Mail does not start
until events.outbox exists. See
Live updates.
Mail pages follow a mailbox on /api/mail/live with the channel mailbox,
which reads the topic cloud:live:mail and checks the reader's access when it
delivers an update. The mailbox view, the composer, and the mailing list dialog
of a page share one socket instead of opening one each, and a returning tab
receives what it missed instead of loading the view again. The socket accepts
every credential the Mail API accepts, so pages in the phone app receive live
updates too.
The first Mail start after the update replaces mail.enqueue_live_invalidation()
and drops mail.live_invalidation_outbox. The function keeps its signature, so
replicas of the previous version that still run during a rolling update write
their changes to the platform outbox as well, and the new replicas publish them.
A tab connected to a previous replica receives no updates until that replica
stops; for one release, /api/mail/ws then closes its socket with
login_required. A mailbox view reloads once. A compose page opened on its own
before the update receives no live updates until it is reloaded; its draft
lease still protects the draft. Previous replicas log
Outbox reconcile failed until they stop, because their table is gone.
Nothing writes the previous topic, mail:invalidations, anymore. Its events
expire after 24 hours, but its stream keeps its 1 GiB reservation until the
next release removes it together with /api/mail/ws; see
Deployment requirements.
If you roll Mail back to the previous version, its pages receive no live
updates until Mail is updated again, because the database keeps the new
function. Reloading a page shows the current state. The updates written
meanwhile wait in events.outbox and are published by the next Mail start.
Files supports Filegate 7
Files is tested with Filegate 7.0; Filegate 6.1 keeps working. Upgrading the
Filegate daemon needs no migration: the index format, the /v1 API, and leases
stay the same, and the root volumes and state_dir stay in place.
Upgrade Cloud before or together with the daemon. Filegate 7 publishes file IDs
only on roots with stable IDs, and earlier Files releases compare the IDs they
recorded under Filegate 6.1. On roots with index: true and managed: false,
those releases cannot finish these operations if they were still open when
the daemon changed:
- moving files and folders into the trash and restoring them from it;
- archiving, restoring, and deleting directories.
Repeating a completed trash restore also fails. These releases report
source_changed or operation_unresolved indefinitely. The current release
compares IDs only when both sides have one and otherwise compares modification
time and size, so these operations complete on their next retry or trash
listing.
Filegate 7 reports stable file IDs (stableIds) only for roots with both
index: true and managed: true. Such a root needs Filegate as its only
writer and readable and writable user.* extended attributes on the real
mount. Files uses these IDs for stable file references; see
Files references survive rename and move.
An ID stays valid only while the file
keeps its device and inode, so back up each root with its .filegate
directory, ownership, ACLs, and extended attributes together with the complete
state_dir, with Filegate stopped. Restoring copies onto new inodes or another
device assigns new IDs. Follow Filegate's backup and recovery guide.
Spaces writes live updates through the platform outbox
Spaces writes its live updates in the transaction that makes the change,
through the platform outbox that Core's migration creates, so a change can no
longer commit without reaching open pages. Update Core before Spaces: Spaces
does not start until events.outbox exists. See
Live updates.
Spaces pages follow a Space on /api/spaces/live with the channel space,
which reads the topic cloud:live:spaces and checks the reader's access when
it delivers an update. For one release, /api/spaces/ws answers tabs that were
open during the upgrade with a request to load the page again; such a tab
reloads once. While replicas of both versions run, a change handled by an older
replica reaches a new tab only after its next reload, and a tab whose page and
socket come from replicas of different versions can reload and then show "Live
updates are unavailable right now" until the rollout finishes; reloading the
page after the rollout restores live updates. Nothing writes the
previous topic, cloud:spaces:events:items, anymore. Its events expire after
24 hours, but its stream keeps its 1 GiB reservation until the next release
removes it together with /api/spaces/ws; see
Deployment requirements.
Mail folders choose where their mail appears
A Mail folder's sidebar switch became a display with three values:
everywhere, folder_only, and hidden. A conversation whose mail lies only
in folder_only or hidden folders leaves All mail, the work views except
Assigned to me and Send problems, their counts, the cross-mailbox overview, and
the agent tools that list them; search and saved views still find it. See
Mail.
Folders that were hidden from the sidebar keep their mail out of those views
from now on, including folders older versions hid on discovery because they
were not subscribed. Set such a folder to everywhere if its mail should appear
there again. Newly discovered folders now always start as everywhere, also
when the account does not subscribe to them, so new shared folders and an
unsubscribed INBOX keep their mail in the views.
Gmail's All Mail, Important, and Starred never decide where mail appears, so hiding them changes nothing. Mail recognizes Important and Starred at its next folder discovery, which runs for every connected mailbox within 15 minutes of the first start. Until then a hidden Important or Starred folder keeps conversations that lie only there and in All Mail out of the views.
The first Mail start after this update adds mail.folders.display, filled from
mail.folders.show_in_sidebar (a shown folder becomes everywhere, a hidden
one hidden), and mail.folders.provider_collection. show_in_sidebar stays
and Mail keeps it current, so replicas can be replaced one by one and an older
image runs on the database without SQL. An older image shows folder_only
folders like everywhere ones and keeps every conversation in the views; its
own sidebar changes reach the newer version only through show_in_sidebar, not
the display. A later release drops show_in_sidebar.
showInSidebar: false in the create_folder command and
cld mail folder create --hide-in-sidebar now create a hidden folder.
| Old | New |
|---|---|
PATCH .../folders/{folderId} with {"showInSidebar": false} |
{"display": "hidden"} (or everywhere, folder_only) |
showInSidebar in folder lists |
display, effectiveDisplay, displayInheritedFromFolderId, displayNeutral |
cld mail folder hide <folder-id> |
cld mail folder display set hidden <folder-id> |
cld mail folder show <folder-id> |
cld mail folder display set everywhere <folder-id> |
Agents set the display with the folder.display.set action; folder.list
returns it.
Grids change-feed cursors need one rescan
cld grids records changes and its HTTP route used to resume after the start
time of a write transaction. A long write that started earlier but committed
later, such as an import, could be skipped. The feed now resumes after the
writing transaction and returns a change only once every earlier write has
ended. Cursors saved before the upgrade answer 409 once: perform a full
Record scan and continue with the new cursor, as for an expired cursor. See
Grids.
Contacts writes live updates through the platform outbox
Contacts writes its live updates in the transaction that makes the change,
through the platform outbox that Core's migration creates. Update Core before
Contacts: Contacts does not start until events.outbox exists. See
Live updates.
The Contacts socket reads the topic cloud:live:contacts. With one Contacts
replica, tabs that are open during the upgrade reload once. While replicas of
both versions run, a change handled by an older replica reaches a new tab only
after its next reload, and a tab whose page and socket come from replicas of
different versions can reload and then show "Live updates stopped" until the
rollout finishes; reloading the page after the rollout restores live updates.
Nothing writes the previous topic, cloud:contacts:events:changes, anymore.
Its events expire after 24 hours, but its stream keeps its 1 GiB reservation
until a later release removes it; see
Deployment requirements.
A moved contact now arrives as a deletion in its source book and a creation in its target book, so a reader of one book does not learn the other. Another tab that shows the moved contact closes its details instead of following it.
Live updates are served by routes(); subscribe() is removed
defineLive() from @k2b/cloud/events returns routes(channels), the
application's live socket, instead of the interim subscribe(). Declare a
channel for each kind of subscription, mount routes(channels) at
/api/<application ID>/live, and subscribe from the browser with
liveConnection() from @k2b/cloud/browser/live. Remove the socket that
read subscribe(). See Live updates and
Realtime UI.
Contacts moved to /api/contacts/live with the channels book and all. For
one release, /api/contacts/ws answers tabs that were open during the upgrade
with a request to load the page again; such a tab reloads once. The next
release removes it.
The legacy Files app is removed
Files (legacy), the files application with the cloud-app-files image and
the /app/files, /api/files, and /admin/files routes, is no longer part of
Cloud. Releases no longer publish its image, and the supplied Compose files no
longer define an app-files service. Files (filesv2)
replaces it.
There is no automatic migration. Files never read the legacy application's
settings or storage, and nothing moves on upgrade. An installation that still
runs app-files must move its users to Files before it upgrades:
- Set up Files with Filegate 7.0 as described in Deployment requirements, and map the home and group directories the legacy app served to Files storage.
- Verify that users see and can open their files in
/app/filesv2. - Remove the
app-filesservice from the deployment. Do not keep an oldercloud-app-filesimage running next to the new release.
Links to /app/files and legacy file references in chats or notes no longer
resolve.
The legacy application owned no database schema; it read the shared identity tables. Upgrading deletes nothing. These records remain until an administrator removes them after verifying Files:
- the
files.*settings, including the encryptedfiles.filegate_token: in/admin/settingson the General tab, review Legacy settings and choose Clean up. The cleanup removes every stored key that no running application registers, not onlyfiles.*, so first check that the list holds nothing you still need. - the offline
filesregistration: in/admin/gateway/apps, choose Remove offline app for Files (legacy).
Once Files serves every directory you still need, stop the Filegate v2 daemon
and remove its configuration and token. Keep its storage: the home and group
directories in its ALLOWED_BASE_PATHS are the directories that Files roots
now point at. Remove a directory or volume only when no Files root references
it, and only after a backup.
Venue objects no longer carry a calendar token
Venue objects in API responses, such as GET /api/venue/venues, and the
cld venue output no longer include icalToken. The venue-wide token had no
route since calendar links became personal, but every reader of a venue
received it. A person's calendar link comes from GET /api/venue/calendar/my.
On startup, Venue drops the unused venue.venues.ical_token column, and an
older Venue image fails to start without it. To roll Venue back to an older
image, first restore the column; no backup restore is needed:
ALTER TABLE venue.venues ADD COLUMN IF NOT EXISTS ical_token TEXT UNIQUE NOT NULL DEFAULT encode(gen_random_bytes(24), 'hex');Every venue gets a new token that no route accepts, and the next upgrade drops the column again. See Venues.
Venue exception notes are public and link addresses are checked
The Venue public page and GET /api/venue/public/{id}/status list the closed
days and special openings of the next 30 days as upcomingExceptions, each
with its note. This includes notes written before the change, which visitors
did not see until now. After upgrading, admins should review the notes of
upcoming exceptions and rewrite any that were meant only for staff.
Links sections accept only https:, http:, mailto:, and tel: addresses
and paths on the same Cloud that start with a single /. Creating or updating
a section with any other address answers 400. A stored link with such an
address, for example www.example.org, no longer appears on the public page;
the workspace preview says how many links visitors miss, and the section
saves again once the address is fixed. No data migration is required. See
Venues.
Service accounts gain standalone and agent kinds
ServiceAccount.kind is now user_delegated, resource_bound, standalone,
or agent. Application code that treated every userless service account as
resource-bound must check kind === "resource_bound" before reading appId,
resourceType, or resourceId; a standalone account authenticates like a
user with its own grants. OAuth clients may bind a standalone account for the
client-credentials grant, and its access tokens carry service_account_kind: "standalone" or "agent". No migration is required: the auth.service_accounts
check constraints widen in place, and existing delegated and resource-bound
accounts, keys, and clients are unchanged. Operators provision agents with
cld admin agents and revoke them by disabling the account. See
Standalone service accounts and agents.
The cloud-cli skill comes from cld
cld update no longer downloads cloud-cli-skill.tar.gz; the core skill is
embedded in cld, and cld writes it together with the installed modules'
references to every target in skills.targets. cld update --skills-dir <dir>
now adds a target instead of choosing one for a single run, and
--claude-symlink adds ~/.claude/skills as a target that receives a copy
instead of a symlink; an existing symlink there is replaced by the copy. The
installer's --skills-dir, --no-skills, and --claude-symlink keep their
meaning and register the targets through cld skills add. The release still
publishes cloud-cli-skill.tar.gz for cld versions before this change. See
Application CLI modules.
A config from cld 0.17 or earlier has no skills entry, because only the
first cld login asked for targets. Updating such an installation to 0.19
therefore wrote no skill, and cld skills list reports no target. cld update
and cld skills sync now ask once when the config has no skills entry;
--yes takes ~/.agents/skills, and without a terminal they write nothing and
print the cld skills add command. To restore the skill, run
cld skills add ~/.agents/skills once, or cld update --yes once cld runs a
release with this change. A target list emptied with cld skills remove stays
empty.
First-party CLI modules are served plugins
cld no longer bundles the application modules (account, admin, apps,
capabilities, accounts, api-docs, assistant, contacts, faq,
filesv2, grids, ipa-hosts, mail, notebooks, oauth, pulse,
spaces, tools, venue). Each Cloud serves them as plugins, and a profile
installs the versions of its Cloud: accept the offer after cld login, or run
cld plugins install --all. A Cloud released before its applications served
plugins offers none; upgrade the Cloud or keep the previous cld for it.
Scripts that ran cld <module> … on a fresh machine must install the module
first. tools needs a Cloud to install from as well. The cloud-cli skill
ships no application references any more; cld <module> reference [file]
prints them from the installed plugin. See
Application CLI modules.
CLI plugin installation
cld plugins install <name> now installs the plugin that the current
profile's Cloud serves. An npm package needs the npm: prefix, for example
cld plugins install npm:@example/inventory-cli@1.4.0; a bare package name no
longer reaches the npm registry. Local paths and .tgz archives are unchanged.
cld plugins list --json returns one row per plugin with profile, name,
app, installed, available, status, and source instead of the package
plugin fields id and package. See
Application CLI modules.
Grids CLI commands
The cld grids commands for bases, tables, and records use the shared cld
verbs. The old names are removed without aliases; scripts must switch to the
new names. Arguments, flags, and --json output are unchanged, and every base,
table, or record argument now also accepts an address such as
<base>:<table>/<record id>. See
Grids.
| Old command | New command |
|---|---|
list, bases list |
bases ls |
bases get, tables get, records get |
bases show, tables show, records show |
bases create, tables create, records create |
bases add, tables add, records add |
bases update, tables update, records update |
bases set, tables set, records set |
bases delete, tables delete, records delete |
bases rm, tables rm, records rm |
tables list, records list |
tables ls, records ls |
Mail CLI commands
The everyday cld mail commands use the shared verbs and addresses. The old
names are removed without aliases; scripts must switch to the new names. A
mailbox is its ID or exact name, a folder is <mailbox>:<path> such as
"Support:Projekte / 2025", and an ambiguous name or path fails with 409
and lists its candidates. Triage commands take up to 50 conversation IDs as
arguments and act on the Inbox unless --in <folder> names another source
folder. Administration, provider, identity, automation, workflow, operator,
repair, and storage commands keep their names. See
Mail.
| Old command | New command |
|---|---|
list |
ls |
conversation list [--folder <id>] |
ls <mailbox>[:<folder path>] |
conversation get |
show <conversation> |
message get |
cat <message> |
conversation assign --conversation <id>... --to <user> |
assign <conversation>... --to <user> |
conversation archive --source <folder-id> |
archive <conversation>... [--in <folder>] |
conversation move <conversation> <folder-id> --source <folder-id> |
mv <conversation>... --to <folder> [--in <folder>] |
conversation trash --source <folder-id> |
rm <conversation>... --yes [--in <folder>] |
conversation read, unread |
read, unread <conversation>... |
conversation star, unstar |
flag, unflag <conversation>... |
conversation tag add --conversation <id> --tag <id> |
tag add <conversation>... --tag <tag> |
| none | tag rm <conversation>... --tag <tag> |
conversation junk, not-spam --source <folder-id> |
conversation junk, not-spam <conversation>... [--in <folder>] |
conversation keyword add <conversation> <keyword> --source <folder-id> |
conversation keyword add <conversation>... --keyword <keyword> [--in <folder>] |
comment list, add, edit, delete |
comments list, add, update, delete |
send --identity --to --subject --body [--attach] |
draft create ..., draft attachment add ..., then send <draft> |
search --folder <name> |
search --folder <path>; a path matching several folders fails instead of searching all of them |
send now sends an existing draft instead of composing one. The new
reply <conversation> and forward <conversation> --to <address> create reply
and forward drafts from the latest message; draft create --intent still
works.
The Mail API adds GET /api/mail/resolve?mailbox=<ref>[&folder=<ref>], which
resolves a mailbox ID or name and a folder ID or path, and answers 404 or
409 with the candidates. No stored data changes.
Files CLI commands
cld filesv2 was rebuilt around shell-like verbs and file addresses. The old
command names are removed without aliases; scripts must switch to the new
names. Files are addressed as <area>:/path or by file ID instead of a base ID
followed by a path, --path, or --to. The area is me, a group's exact name
or ID, or a full area ID from ls; an ambiguous name fails with status 409 and
lists every candidate. Administrator commands use --storage instead of
--area and address directories as <storage>/<users|groups>/<name> or
<storage>/archive/<archive-id>. See
Files.
| Old command | New command |
|---|---|
bases list |
ls |
list <base> --path <path> |
ls <area>:/<path> |
stat <base> <path> |
stat <file> |
download <base> <path> --out <local> |
get <file> [<local>] |
archive <base> <path>... --out <local> |
get <folder> [<local>], zip <entry>... --out <local> |
upload <base> <local> --to <path> |
put <local> <file> (--parents creates folders) |
mkdir <base> <path> |
mkdir [-p] <folder> |
rename <base> <path> <name> |
mv <file> <new-file> in the same folder |
move <base> <path>... --to <folder> |
mv <entry>... <folder>/ |
copy <base> <path>... --to <folder> [--target-base <base>] |
cp <entry>... <folder> |
delete <base> <path>... |
rm <entry>... --yes |
search <base> <query> --path <path> |
search <folder> <query> |
trash list <base>, trash restore <base> <id> |
trash list <area>, trash restore <area> <id> |
versions list <base> <path> |
versions list <file> |
versions download <base> <path> <id> --out <local> |
versions get <file> <id> <local> |
versions restore <base> <path> <id> |
versions restore <file> <id> |
versions comment <base> <path> <id> <text> |
versions update <file> <id> --comment <text> |
shares create <base> <path>... |
shares add <entry>... |
shares revoke <id> |
shares revoke <id> --yes (unchanged name) |
favorites add|remove <base> <path> |
favorites add|remove <entry> |
documents create <base> <path> |
documents create <file> |
documents markdown <base> <path> |
documents create <file> --kind markdown |
edit-url <base> <path> |
edit-url <file> |
thumbnail <base> <path> --out <local> |
thumbnail <file> <local> |
templates get <id> |
templates show <id> |
templates use <id> <base> <path> |
templates use <id> <file> |
admin templates import <base> <path> |
admin templates import <file> |
admin … --area <cloud|freeipa> |
admin … --storage <cloud|freeipa> |
admin files list --area --kind --name|--archive-id --path |
admin files ls <directory>[:/<path>] |
admin files download <path> … --out <local> |
admin files get <directory>:/<path> <local> |
admin files delete <path> … |
admin files rm <directory>:/<path> --confirm-path … --yes |
admin versions list|delete <path> … |
admin versions list|delete <directory>:/<path> … |
admin directories archive|retire|delete <name> --area --kind |
admin directories archive|retire|delete <storage>/<kind>/<name> |
admin shares revoke <id> |
admin shares revoke <id> --yes (unchanged name) |
tree and cat are new. recent, favorites list, shares list,
templates list, and the remaining administrator commands keep their names.
The Files API adds GET /api/filesv2/entries/:id, which resolves a file ID
with the same access checks as a path. No stored data changes.
Contacts CLI commands
cld contacts now uses the shared command verbs and addresses contacts by
ID or <book>:<name>. The old command names are removed without aliases;
scripts must switch to the new names. The --book and --contact flags and
the local default book (use, current) are gone: pass the book in every
address. show --email <address> finds a contact by email. Contact JSON input
is --from <file|-> instead of --json-input and --stdin; note text is
--from <file|-> or --content <text> instead of --file and --stdin.
Destructive commands need --yes without a terminal. See
Contacts.
| Old command | New command |
|---|---|
books |
ls |
use, current |
removed; pass the book in every address |
book |
show <book>: |
create-book [--use], update-book, delete-book |
books add, books update, books delete |
list |
ls <book> (--q, --tag) |
get |
show <contact>, show --email <address> |
create |
add <book>[:<name>] |
update |
set <contact> |
move --target-book |
mv <contact> <book> |
delete |
rm <contact> |
notes, note, update-note, delete-note |
notes list, add, update, delete |
tags, create-tag, update-tag, delete-tag |
tags list <book>, tags add|update|delete <book>:<tag> |
import-preview |
import <book> --from <file|-> --dry-run |
--query |
--q |
--output |
--out |
--firstName, --per_page, and other camelCase or snake_case aliases, --parent-contact |
the kebab-case flag (--first-name, --per-page), --parent |
import without --dry-run now creates the previewed contacts, skipping
matches unless --include-duplicates is given. search, tree, export,
and the access group keep their names. The Contacts API adds
GET /api/contacts/resolve. No stored data changes.
Notebooks CLI commands
cld notebooks was rebuilt around note addresses and a local Markdown mirror.
The old command names are removed without aliases; scripts must switch to the
new names. Notes are addressed as a note ID, <notebook>:<path>, or a file in
a pulled mirror instead of --notebook and --note flags, and the local
default notebook (use, current) is gone. Content input is --from <file|->
or --content <text> instead of --file, --stdin, and --content. See
Notebooks.
| Old command | New command |
|---|---|
list |
ls |
use, current |
removed; pass the notebook in every address |
get |
stat <notebook>: |
notes |
ls <notebook>[:<path>] |
note, content, read |
stat <note>, cat <note> (--json, --numbered, --blocks) |
block |
cat <note> --block <name> |
create-note |
write <notebook>:<path> [--parents] |
edit --set-content |
write <note> |
move-note |
mv <note> <target> |
copy-note |
cp <note> <notebook>[:<path>] |
delete-note |
rm <note> |
lock-note |
lock <note> |
search --all, tag-notes |
search [query] [--notebook] [--tags] |
favorite, unfavorite, favorites |
favorites add, favorites remove, favorites list |
comments, add-comment, update-comment, delete-comment |
comments list, add, update, delete |
versions, version, restore-version |
versions list, versions cat, versions restore --into |
upload-attachment |
attach <note> <file> |
attachments, download-attachment, delete-attachment |
attachments list, download, delete |
attachment, attachment-usage |
attachments list --json, usage shown by attachments delete |
create-from-template |
create <name> --template <id> |
api-keys, create-api-key, revoke-api-key |
api-keys list, create, revoke |
snapshot, update-snapshot, snapshot-logs, run-snapshot |
snapshots show, set, logs, run |
--output-file |
--out |
cld notebooks create now creates an empty notebook without the welcome note;
the web UI still seeds it. The Notebooks API adds GET /api/notebooks/notes/:noteId,
GET /api/notebooks/:id/outline, and GET /api/notebooks/:id/resolve, and
POST /api/notebooks/:id/notes accepts parentPath and createParents. No
stored data changes.
Spaces CLI commands
cld spaces now uses the shared cld verbs and item addresses. The old
command names are removed without aliases; scripts must switch to the new
names. An item is addressed as an item ID or <space>:<title> instead of a
leading space argument or --space, and the local default space (use,
current) is gone. Text input is --from <file|-> instead of --file and
--stdin; --page-size is --per-page. See
Spaces.
| Old command | New command |
|---|---|
list |
ls |
use, current |
removed; name the space in every address |
get [space] |
show <space>: |
create |
create (no --use) |
items [space] |
ls <space> |
items --assigned-to me, unassigned |
ls <space> --mine, --unassigned |
items --deadline, --activity inactive |
ls <space> --due, --inactive; new --due-before |
item [space] <item> |
show <item> |
add-item [space] <title> --column |
add <space>:<title> [--column] (defaults to the first column) |
update-item [space] <item> |
set <item> |
update-item --column |
mv <item> <column> |
| none | rm <item> --yes, assign <item> <user|me|none>, due <item> <date|none> |
blockers, blocks |
deps <item> |
block <task> <blocker> |
deps <task> --add <blocker> |
unblock <task> <blocker> |
deps <task> --rm <blocker> |
comments |
comments list |
comment |
comments add (new comments update, comments delete) |
attachments |
attachments list |
add-attachment --file <path> |
attachments add <item> <path> |
download-attachment --output |
attachments download <item> <attachment> --out |
delete-attachment |
attachments delete |
checklist add --label <text> |
checklist add <item> <text> |
references remove |
references delete |
calendar --from --to |
calendar <start> <end> |
overlap --from --to --exclude-item |
overlap <start> <end> --exclude <item> |
activity, work, claim, release, progress, done, reopen, invitation context, invitation draft, access … |
unchanged names; the item is one address argument |
--file, --stdin |
--from <file|-> |
--page-size |
--per-page |
The Spaces API adds GET /api/spaces/items/:itemId and
GET /api/spaces/resolve?space=&title=; the item filter accepts
deadlineBefore, and assignable users include uid. No stored data changes.
Workflow action costs
Move budget counts from plan().consumes into the action's cost(ctx, config)
hook. plan() now produces only dry-run summaries, issues and optional output;
it is never called during execution. Both modes use cost() for budget counts.
Update all action declarations together with the platform; there is no fallback
to plan().consumes. No stored workflow data migration is required.
See effect budgets.
Contextual Universal Search
Search Queries now accept an optional resource scope in the canonical input
schema and may declare local scopeTypes. Update all search-provider images
together with the platform; old manifests do not match the new shared schema.
No stored data changes are required. Use
the shared browser search API
in place of app-local navigation Spotlight dialogs. Selection pickers remain
separate.
Production configuration cleanup
Built-in images use production mode and port 3000. Remove runtime APP_ID
and PORT overrides; application identity comes from its declaration. Build
and development scripts still accept APP_ID to select an application.
FreeIPA no longer imports environment configuration. Configure the freeipa.*
settings, including group rules, in administration. Remove Cloud-side
FREEIPA_*, GROUPS_*, FILEGATE_URL and FILEGATE_TOKEN bootstrap inputs. Filegate's own server configuration remains separate.
Grids query limits moved to grids.query_pool_size, grids.query_concurrency,
grids.query_queue_limit and grids.query_queue_timeout_ms on its admin page.
Remove the corresponding GRIDS_QUERY_* environment variables. Restart all
Grids instances after saving these settings.
Mail's unreleased Google/Microsoft managed OAuth integration has been removed:
provider environment variables, mail.oauth.* settings, browser callbacks,
automatic token refresh and reconnect flows are no longer available. Users
create IMAP/SMTP connections with a password, app password or manually supplied
access token. Manual access tokens are not renewed automatically. No migration
of old alpha managed OAuth connections is provided; use a fresh Mail schema.
The Cloud OAuth application and its client integrations are unchanged.
Mail's alpha migration chain (versions 1 to 126) was collapsed into a single
baseline schema. Mail has never been deployed, so no upgrade path exists: a
development database that still holds the old versions is refused at startup and
is reset with DROP SCHEMA mail CASCADE;.
Pulse is included in production Compose and the release image set. Fresh Core
installations use an explicitly supplied temporary ADMIN_LOGIN_TOKEN for
first access; see Deployment requirements.
Grids starts with a fresh schema
Grids creates its current schema at startup. There is no in-place migration from older Grids schemas. Before switching an existing installation, the operator must stop Grids and explicitly reset its schema and Grids-owned shared workflow and access data. This discards the existing Grids content; keep any required backups or exports separately. Delete only Grids-owned rows in shared Core tables, never the shared schemas or another application's data. Start Core before Grids. Subsequent Grids starts preserve the current data.
Reset an existing Grids installation
This procedure permanently discards Grids content, workflow runs, generated documents, and resource-bound credentials. It is not an upgrade that preserves data. Confirm the target database and NATS namespace, obtain approval for this scope, and keep a restorable backup before starting. A schema-only reset is incomplete: old kernel runs would still appear in Grids health reports.
Stop every Grids replica and worker, and prevent new Grids invocations during
the maintenance window. Inventory foreign keys and cross-application references
before deleting anything. If another application references a Grids resource or
service account, stop and resolve that dependency; do not let CASCADE remove it.
Perform the database cleanup in one transaction, in this order:
- Capture access IDs from
grids.base_accessandgrids.custom_app_access, Grids resource-bound service accounts and their grants, and kernel workflow and event IDs whereapp_id = 'grids'. Retain this inventory until verified. - Check that captured grants and service accounts are not used by another application. Preserve shared grants and ordinary user accounts.
- Delete
workflows.event_deliveryrows referencing the captured workflow or event IDs. Delete rows fromworkflows.workflow,workflows.event, andworkflows.dependency_signalonly whereapp_id = 'grids'. Their kernel child rows are removed through foreign keys. - Delete
capabilities.idempotency_claimsandcapabilities.executionsonly whereapp_id = 'grids', so old results cannot be replayed. - Run
DROP SCHEMA grids CASCADEonly after the dependency checks above. - Delete captured, exclusively Grids-owned
auth.accessrows. Remove mandates only whereowner_app_id = 'grids', then the captured resource-bound service accounts and their credentials. Do not delete delegated user accounts. - Commit. On an unexpected dependency or error, roll back and investigate.
PostgreSQL and NATS cleanup are separate operations. Keep Grids stopped until
both finish. Inventory streams by their Sync metadata, not a guessed name
prefix. Delete only streams with the verified sync.namespace,
sync.owner=grids, and these resource IDs, including their associated
key-value and dead-letter streams:
grids:workflow-record-eventsgrids:evidence-exportgrids:controlled-destructiongrids:workflowsgrids:external-record-operation-retentiongrids:recordsgrids:metadatagrids:workflow-runtimegrids:workflow-runs
Do not delete shared volumes, other namespaces, or other applications' streams. If transport cleanup fails, leave Grids stopped and resume from the inventory; do not start new workers against old queued work.
Start the updated Core before the updated Grids. Check gateway registration, an authenticated Grids page, empty Base/Record/Document lists, and the health view for leftover Grids runs. Confirm a second Grids start preserves the schema and any newly created data. Old Grids links in other applications remain but point to removed resources; do not delete their chats or audit history.
Mail automation authority is mandate-only
Mail incoming automations no longer create, store or forward user API tokens.
Spaces actions require a mandate and Mail's app-bound identity:invoke
credential. The authority-mode flag and credential backfill have been removed.
Mail is unreleased alpha, so this change does not convert old automation data. Use a fresh Mail schema when replacing an older alpha installation. Existing test data and previously issued credentials are not deleted automatically; resetting them is a separate operator action, not an application startup step.
Notebook scripts are now inert Markdown
Notebooks no longer executes fenced script blocks. Existing source remains
in each note as visible code. Use :::toc for an in-note contents list and
:::query with named :::data properties for notebook summaries. Update
automation that sends scriptsEnabled or uses --scripts-enabled; the
setting and script-specific APIs are removed.
The shared markdown.render() and markdown.renderSync() helpers also render
script fences as ordinary code blocks. They no longer emit executable source
carriers or output containers. Application authors must remove any custom
enhancer that depended on those carriers; Markdown rendering does not execute
user code. Help examples remain inert.
Identity authority rollout prerequisites
Deploy Core's purpose-specific session and invocation JWKS endpoints before their consumers. Every Core replica behind the configured JWKS origin must support them, including when Core is itself a consumer. Mixed old/new endpoint pools can fail on cold caches. Drain every old replica before the coordinated hard cut; no issuance-mode or old-writer fallback remains. See the network, database, clock, and rollout requirements in Runtime configuration.
The public gateway now rejects HTTP and WebSocket paths containing an
_internal segment. Broker callers must use the private Core origin. This is
an ingress boundary, not a substitute for workload authentication.
Confirmed-only mandate coordinates require a coordinated cutover
The mandate uniqueness index now covers confirmed active or paused rows only.
This prevents pending registrations from reserving another workload's identity.
The previous targeted ON CONFLICT statement cannot run against that new
index and fails with PostgreSQL 42P10. This schema change is not compatible
with old mandate-writing processes.
Before starting a Core version that migrates this index:
- stop mandate-writing Core and application processes, including background workers and separately deployed applications using the old Cloud package;
- update every writer to the version using
ON CONFLICT DO NOTHING; - start updated Core to migrate, then start the updated application writers;
- verify interactive creation, pending confirmation, and background delivery before restoring workload creation traffic.
Do not restart old mandate writers against the new schema. An operator needing a rolling upgrade must first deploy a compatibility release that changes only the insert conflict handling on every writer, then deploy the index migration. This mandate-index change does not itself revoke credentials. The JWT-only browser and OAuth hard cut below must be coordinated with it.
Widget response validation and budgets
Widget responses remain extensible: unknown fields are stripped. Safe relative links and absolute HTTP(S) links remain accepted, while active schemes and oversized or malformed payloads are rejected. Dashboard runs eight requests at a time with 500 ms per started widget and a page budget derived from the number of waves. See Dashboard widgets.
JWT-only sessions and internal invocations
Use the coordinated upgrade checklist for preparation, startup order, verification, and rollback. The contract below defines what changes for sessions and OAuth clients.
This is a coordinated hard cut, not a rolling compatibility release. All Cloud users must sign in again. OAuth client registrations and secrets, API keys, and background mandates are retained. OAuth sign-ins also end once on upgrade.
- Back up PostgreSQL and the Core identity-key encryption key. Drain old Core and application replicas, including background workers, before migration.
- Deploy Core and every application with the JWT-only release. Keep the Core-only key encryption key configured and both purpose-specific JWKS endpoints reachable. There is no browser or invocation issuance-mode switch.
- Core's migration revokes existing JWT browser families once and removes the legacy generation column. Opaque Valkey sessions are always rejected. Repeated migrations preserve new logins and do not change user epochs.
- Verify a new login, logout, WebSocket reconnect, search, widgets, and an existing OAuth client before restoring traffic and background work.
Do not roll an old binary back onto the migrated schema. Recovery requires a coordinated compatible release or restoration of the backed-up database and keys. Do not flush shared Valkey: unused opaque session keys expire naturally; old generation keys are no longer read or written.
Application code must use request actor and accessSubject, not parse or
persist session tokens. session.createDelegation, session.getData,
session.parseToken, and the legacy forwarding middleware are removed.
Use session.authenticate for live session reauthorization and the public
capability/mandate APIs for cross-application work. Internal capability and
widget endpoints reject browser cookies, OAuth tokens, and API keys; Core
issues target- and operation-bound invocation JWTs for them.
OAuth signing hard cut
OAuth no longer accepts CLOUD_APP_CREDENTIAL or the Compose input
CLOUD_OAUTH_APP_CREDENTIAL. Replace them with CLOUD_OAUTH_BROKER_SECRET on
Core and OAuth; see Runtime configuration.
Revoke any previously provisioned OAuth workload credential through the admin
identity API. The retired identity:oauth-issue scope is no longer provisionable
and remains blocked at ordinary API entry points. Client secrets are unrelated
and do not need replacement.
Include every OAuth replica in the maintenance window. Configure the same
CLOUD_OAUTH_BROKER_SECRET exclusively on Core and OAuth, start updated Core,
then start updated OAuth. Its migration drops only the obsolete oauth.keys and
oauth.issuance_state tables and the old code-audience compatibility trigger.
Client registrations and client secrets are retained. Older authorization codes
cannot be redeemed, and refresh families without an epoch are revoked; see
OAuth sign-ins end once on upgrade.
Old OAuth access and ID tokens are no longer supported by the updated Cloud. Clients need another authorization to obtain Core-issued tokens. No supported OAuth grant type, PKCE, dynamic registration, consent or resource-binding feature is removed. Clients using discovery do not need a new client ID or secret. A client that manually pins signing keys must load Core's keys from the unchanged public JWKS URL. An external client's cached old keys are not remotely erased by this update.
The combined /.well-known/cloud-identity-jwks.json endpoint is removed.
Current applications use the separate session and invocation JWKS endpoints.
Do not restart old binaries against this schema. Restoring removed signing
material requires the pre-upgrade database backup; there is no signing-mode
rollback switch.
Scheduled AI/chat tasks without a stored mandate no longer acquire one during
background execution. Admission marks them needs_attention; the owner must
delete and recreate them. Existing mandated tasks are unaffected. This removes
the system-migration authority path, including the obsolete Mail variant.
Conversation files use one namespace
The alpha /input versus /files path policy was removed. Uploads and
assistant-created artifacts now share the absolute conversation namespace, and
the durable origin field owns overwrite policy. Turn payloads reference every
attachment, including images; inline base64 image parts and the CLI
--workspace upload switch were removed without a compatibility shim.
Deprecated APIs remain for source compatibility.
Do not use them in new code. Migrate one boundary at a time and keep behavior covered by tests.
Server helpers
| Old | Current |
|---|---|
validator |
v |
untyped apiClient |
api.create<TApi>() |
validator is an alias. Replace the import and keep the existing schema.
The old apiClient is untyped. Export the final Hono router type, then create a
typed browser client with the real base URL.
See Browser clients.
Access inputs
getEffectivePermission() still accepts userId, userGroups, and
serviceAccountId.
Pass subject instead.
await getEffectivePermission({
accessIds,
subject: c.get("accessSubject"),
});userGroups is ignored. Cloud resolves direct and nested membership from the
authoritative platform tables.
See Authorization.
Encoded JSON metadata upgrade
For upgrades from the June 23, 2026 production baseline, the startup migrations automatically repair encoded deleted-account, audit, and Venue objects. They discard malformed Tools logs and Gateway registry snapshots, and cancel notification batches with invalid selection containers. Valid data survives repeated startup. Stop old writers before starting updated services; no manual repair command is required. See JSON metadata upgrade for the preservation rules and optional diagnosis. The NATS, identity, and OAuth release prerequisites still apply independently. Contacts can no longer be upgraded directly from that baseline; follow the Contacts short-ID upgrade path.
Notification email uses outgoing mail
Notification email and batches now appear in the outgoing-mail send log as app
core. Configure a default sender profile and keep Core's outgoing-mail runtime
running. Notification batches follow that profile's pace; set it to the
provider's limit. The old direct-SMTP notification path has been removed.
Temporary SMTP failures keep email pending while outgoing mail retries it. Recommended fallback channels activate only after a terminal mail failure, which can take up to the 24-hour delivery deadline. They stay deferred during SMTP retries to avoid delivering through both channels if SMTP recovers.
The migration adds outgoing-mail IDs to deliveries, batch recipients, and legacy message rows, plus send generations for legacy messages. Recovery updates legacy history when accepted mail settles; resending in-flight mail reuses it. Existing notification history remains intact. See Outgoing mail operations.
Notification delivery upgrade
When replacing the queue-based notification runtime, stop old application instances before starting the new version. Preserve notification tables: startup recovery resumes accepted pending deliveries and scheduled retries. The job-based runtime does not consume old queues. Remove their transport resources only after verifying delivery recovery.
Notifications
The email-only notifications.send(params) overload and
notifications.sendToUser() are deprecated.
Declare a typed notification in defineApp({ notifications }), then send the
bound definition:
await notifications.send(app.notifications.stockLow, {
recipient: { userId },
data: { itemId, itemName, remaining },
idempotencyKey: `stock-low:${itemId}:${thresholdVersion}`,
});This adds runtime validation, recipient policy, channel selection, and delivery history.
See Notifications.
UI
| Old | Current |
|---|---|
DockWorkspace |
Panes with application-owned layout state |
DateTimeInput |
DatePicker or DateTimePicker |
SettingsModal.subtitle |
Section descriptions |
SettingsModal.icon |
Tab icons |
DockWorkspace remains only for legacy screens. Do not extend its persistence
format.
Use the UI catalog to inspect the current UI contract.
Shared utilities
Import generic utilities directly from @k2b/stdlib.
@k2b/cloud/shared continues to re-export dates, calendar,
encoding, fileIcons, and gradients for older applications.
Cloud-specific shared helpers remain on the Cloud path.
AI names
| Old | Current |
|---|---|
AiDataPolicy |
AiDataBoundary |
startAiRuntimeRecovery() |
startAiRuntime() |
aiConversationStore |
aiConversations |
The alpha AI service and runtime renames are hard cuts; there are no compatibility aliases.
Remove compatibility code safely
- search application source for the old symbol;
- migrate and test each call site;
- run the standalone package typecheck;
- verify browser and server bundle boundaries;
- remove local adapters that only supported the old shape.
The current package version does not assign removal dates to these APIs.
Help moves to Postgres
Update Core and every Help-producing application to the same new Cloud release. This is a coordinated breaking change; old processes still write the retired NATS Help registry and cannot populate the new store.
- Prepare the new application images and the existing Postgres connection.
- Run Core setup to create the Help schema and start the updated Core readers.
- Restart every application with the updated Cloud package. Each application publishes its packaged Markdown before advertising readiness.
- Verify an article, a search, and an AI or MCP Help read in the intended language.
No export or import of the old NATS Help registry is required. Keep app-owned
Markdown and app.start({ help }). The dedicated Help ephemeral and its
helpRegistry, getHelp, listHelp, and HELP_REGISTRY_CONFIG exports are
removed, along with the old registry corpus types and resolveHelpManifest.
Apps should use the automatic Help surfaces instead of those platform internals.
Runtime app metadata now contains a Help reference rather than article lists.
The article endpoint returns HELP_NOT_FOUND (404) for an absent article or
published version; the old HELP_STALE response is retired. Search returns no
matches for an absent app or version. Database failures still return errors.
Core owns the SQL schema. If it is missing, rerun Core setup before restarting applications. Existing app heartbeats restore missing collections once the schema is available. See In-product Help for search, limits, and lifecycle behavior.
Do not upgrade a Postgres volume merely to enable optional BM25. Native search works without that extension. Rolling back requires a consistent old image set and application restarts to repopulate its NATS registry; retained SQL rows do not make old readers compatible. Retired NATS resource removal is a separate operator action after verifying the cutover.
Workspace mobile navigation
The AppWorkspace.SidebarMobileTrigger, SidebarMobile, SidebarMobileItems,
and SidebarMobileBody slots and their prop types have been removed. There is
no compatibility renderer. Use createNavigation and let the host render
Navigation; Cloud applications bind it through WorkspaceNavigationProvider
or the SSR WorkspaceNavigation island. Keep SidebarDesktop for desktop.
See Application shells.
Capability protocol 2
Capability manifests now include a required commands array. Providers declare
protocolVersion: 2; compilation emits an empty array when no Commands are
published. Protocol 1 manifests are rejected, with no compatibility adapter.
Upgrade the shared package and every provider together. The existing
/capabilities/v1 HTTP routes remain unchanged; the manifest protocol is a
separate contract. No database migration is required.
Mail's interactive task/event creation now opens the owning Spaces form through
a Command. The old Mail POST .../conversations/:conversationId/spaces/items
and GET .../spaces/:spaceId form-support routes are removed. Existing resource
link/unlink operations and calendar automation continue to use their domain APIs.
Application keyboard shortcuts
Register browser context commands with an optional shortcut instead of
application hotkeys.create() calls or window key listeners. Remove the old
registration when moving each action; retaining both executes it twice.
Shortcut-only event relays should call the existing command or search API.
See Context command shortcuts.
The portable UI package no longer exports isSpotlightShortcut or
SPOTLIGHT_SHORTCUT* constants. SpotlightButton has no implicit shortcut
label. Supply shortcutLabel only when the host actually owns that binding.
Outgoing mail sender profiles
The mail.noreply.* setting definitions and /api/admin/core/settings/test-email
route are replaced by outgoing mail profiles and the per-profile test route.
Core imports configured legacy SMTP settings automatically.
See Outgoing mail operations for the upgrade
and rollback rules.