Deprecations
Contract deprecations, compatibility windows, and breaking changes for MCP and HTTP clients.
What has been deprecated in the MCP tool and HTTP contract, when, and what to use instead. This ledger is the notice period: anything listed here still works until the stated release, so an existing client has time to move.
Version numbers below are values of CONTRACT_VERSION (exported from
@workspace/contracts, and announced on the MCP initialize handshake as the
zero-memory/contract experimental capability). It is semver over the contract
itself, independent of any package version:
- major — a breaking change: a field removed or renamed, a type narrowed, a tool withdrawn.
- minor — an additive change: a new optional field, a new tool, a new enum member in a field clients only read.
- patch — wording only.
Rules for adding an entry
- Add the row in the SAME change that introduces the replacement — a deprecation without a documented successor is just a removal announcement.
- Keep the deprecated surface working. Deprecating is a promise, not a removal.
- Removal lands no earlier than the next major, and never in the release that announces the deprecation.
- Announce in the release notes as well; a client that never reads this page still needs to hear about it.
Deprecated surfaces
| Surface | Deprecated in | Replacement | Removal no earlier than |
|---|
Nothing is deprecated yet.
Breaking changes
| Change | Version | What a client must do |
|---|---|---|
card create and promote_loop are refused with links_required unless the call states the card's relations. | 4.0.0 | Pass links ([{card, relation, reason}], where card is an id or a ZM-N label) or no_links with a reason why the card relates to no other card. The refusal names candidate cards from the same board. |
card create and promote_loop are refused with type_required unless the call passes type. | 5.0.0 | Pass type: story (a feature from the user's side), bug (a product function is broken), task (neither: docs, tests, refactoring, ops) or spike (the output is an answer, not a change). |
The delete_account tool is withdrawn. It erased the whole account in one call, and an agent the person allowed acts as them on every tool; withdrawn at once for that reason, without a notice period. | 6.0.0 | Nothing for agents: deleting an account is now the person's own step, under Settings → Delete account in the dashboard. A call to the old tool is answered as an unknown tool. |
recall.k is capped at 100, ingest_conversation.transcript_chunk at 200 000 characters and ingest_conversation.recalled_ids at 200 entries; a larger value is refused with validation_failed. Dynamic client registration takes at most 8 redirect_uris of at most 2048 characters, and scope / resource are bounded too. | 7.0.0 | No known client comes near these: the watcher splits transcripts at 24 000 characters and no recall asks for more than a page. A script that sent more pages with smaller k, or splits its chunk. |
The ledger starts counting from the first public release. Contract history from before any external client existed is not compatibility anyone must migrate across, so it is not carried here.
Behavioural changes
Changes that leave the contract's shape untouched — no field moved, no type narrowed, no code added to the vocabulary — but change what a client observes. They carry no version bump, because there is nothing to migrate; they are listed so a client that branched on the old behaviour can find out why it changed.
| Change | What a client should know |
|---|---|
card move into active — and create / promote_loop straight into it — is refused with branch_required unless the call passes branch or no_branch, or the card already holds an open branch. | Pass the work's branch ({repo, name}) or no_branch with a reason. promote_loop opens in active by default, so it is covered too. |
card move out of active is refused with branch_open while the card holds an open branch. | Record the landing with the new land action, or pass not_landed saying why the branch has not landed. |
Cards are labelled ZM-N instead of #N in briefing text, server messages and the dashboard. | The number itself is unchanged and board resolve still takes it; only text that quoted #N reads differently. |
card move into active is refused with links_required when the card's relations were never assessed: no statement at creation or on an earlier move, and no declared relation. | Pass links or no_links on that move, or relate the card with link beforehand. A card that stated its relations once is not asked again. |
card move into active is refused with type_required when the card has no declared type: it was created before cards had types. type on a move only declares a card that has none; a different type on a card that has one is refused as invalid. | Pass type on that move, or set it beforehand with edit. A card that has a type is never asked again; its type changes only through edit. |
card attach with ref_kind: card records an undeclared relates_to relation between the two cards, and detach retires it. | The attachment still works as before. The relation shows up in board get as undeclared, and a declared relation between the same cards replaces it. |
A project_hint that is a project name (no slash, not a git remote) only points at a project the caller already belongs to, matched by spelling, alias or initials. A name that fits none or several of the caller's projects is refused with project_hint_unresolvable: on remember, move_memories, import_memory and card, and treated as absent on reads. It used to create a new project named after it. | To start a new project, pass its repository's path or git remote, which still sets one up on first sight. The refusal lists your projects to retry with. |
card create with neither scope nor project_hint goes on the board of the session's project instead of being refused. | A session with no project is still refused, now with scope_target_required: and the list of your projects. |
A write to memories through the database API (PostgREST, as a signed-in user) is refused with 42501 when it changes owner_id, id or created_at; when it makes a memory shared, or moves a shared memory, into a scope the caller cannot write; when it names someone other than the caller in invalidated_by or shared_by, on insert or update; or when a writer member changes anything on another member's memory other than retiring it in their own name. | Only the owner edits, shares, moves or supersedes a memory, and only into scopes they can write. A writer member may still forget or close_loop another member's shared memory. No one reassigns a memory to another owner. |
A promoted rule with a curated text, carried to a successor memory whose content is exactly that text, is reported as carried under remember's rules and is no longer flagged for review. It used to be carried_text_kept, flagged. | Re-anchor a rule by superseding its memory with one that holds the rule's text; no promote_rule is needed afterwards. |
GET /oauth/authorize no longer renders a password form: it answers 302 to the dashboard's /oauth/consent page, where the person signs in (password, GitHub or Google) and allows or denies the client. POST /oauth/authorize is gone (404). A client the person allowed before is sent back to its redirect URI from the dashboard without a screen. | Nothing changes for a client that opens the authorize URL in a browser and waits for its redirect URI, which is every MCP client. Scripts that posted credentials to the form sign in to Supabase instead and call the consent endpoints the dashboard uses. Existing tokens and registrations keep working. |
A consent covers only the return address the person approved, and a repeat authorization skips the consent screen only when the browser arrived on its own or from the dashboard: GET /oauth/authorizations/:id answers with the request's details unless called with ?screenless=1, which the dashboard adds for such an arrival. Dynamic client registration refuses redirect_uris that mix https addresses with loopback or private-use ones (400 invalid_redirect_uri). | Nothing changes for an MCP client that opens the authorize URL and waits for its redirect URI. Each person is asked for one more Allow the first time a client approved before this change reconnects. A client that needs both a remote and a local return address registers once for each. A script that stands in for the dashboard passes ?screenless=1 on the read and must not expect a redirect otherwise. |
On an account with a verified second factor, a session that has not given the code reaches no data: the memory server answers 401 with error: insufficient_aal, the database API shows it no row and accepts no write (a restrictive policy complete sessions only on every table), and the dashboard sends a navigation to /mfa/verify and answers other requests 403. Sessions the authorization server opens for agents are recorded and stay complete without a code; a session that was opened for an agent is refused by the dashboard with 403. | Nothing changes for an MCP client that authorized through the consent screen. A script or client that used a password or dashboard session of an account with two-factor turned on now authorizes as a client, or gives the code first. Agents connected before the account turned two-factor on authorize once more. |
card (title, body and every reason), card_log note text, the entity names given to link, and the rule_text given to promote_rule refuse text that contains a secret, with validation_failed and the secret_content_rejected: prefix, as remember always has. | Rephrase without the credential and retry. Nothing is stored on a refusal. |
list_conflicts applies limit to the caller's own conflicts, so a page is no longer short when other accounts have newer ones. resolve_conflicts with a policy counts in skipped only the caller's conflicts it could not settle; other accounts' conflicts were never touched and are no longer counted. restore_memory removes only the caller's own supersedes links to the restored memory; another member's link stays. | Nothing to change for a client. A script that compared skipped against the queue size of the whole instance now sees the caller's numbers only. |
search_memories, find_similar_memory and memory_version_history are no longer executable without a session: the database API answers 42501 to an anonymous call. A signed-in caller is unaffected. | Call them signed in. They returned no rows to an anonymous caller before, so nothing that worked is lost. |