# SDK changes — what moved between versions Every change that needs your attention, newest first. Which version to load: [Versions](./sdk.md#versions). Part of [the SDK](./sdk.md) · every call: [the reference](./sdk-reference.md#the-surface). --- ## Changes that need your attention {#changes} Production apps import a fixed version (`/sdk//`): a released version never changes, so an upgrade happens only when you change the number in the address. A page that imports `/sdk/latest/` or the live build (`/witbitz-sdk.js`) gets each change on its next load — use those for development and demos only ([Versions](./sdk.md#versions)). Every release, and every change that can break existing code, is listed here, newest first. **2026-10-27** - **SDK 3.10.0 — Safe media. Nothing breaks.** - `createSpace({ safeMedia: true })`, `configure({ safeMedia })` and `space.safeMedia.set(on)`: others' photos and PDFs are shown only from copies an attested enclave made safe, as in the Spaces app ([Safe media](./sdk-rooms.md#safe-media)). - `space.safeMedia.copy(message, { at, i })` returns a copy only once it verifies on the device; `.isOriginal(copy, b64)` checks the original before you open it; `.make(message)` makes a copy for everyone. `send()` makes the copies of what it sends while the setting is on (a signed-in session). - Events `'safeMedia'` and `'safeCopy'`. A copy is never a message: before 3.10.0 a Safe media room's copies were listed in `messages` as entries with `kind: 'safe'` — filter them out if you stay on an older version. **2026-10-26** - **SDK 3.9.0 — temporary rooms, page tools on a plain room, a site's visitor limit; and the sync sweep's fixes. Nothing breaks.** - `createSpace({ temporary: true | { days } })`: a room the deployment wipes when idle — `{ days }` (1–7) keeps it that many days after its last question; `space.temporary`. Refused rather than faked: `temporary_unavailable`, `invalid_temporary`, `vault_unsupported` ([Temporary rooms](./sdk-rooms.md#temporary)). - `space.send(text, { tools })`: your page's own functions for one question on a plain room, in a lane's action shape ([Page tools](./sdk-assistant.md#page-tools)). - `admin.billing.visitorLimit({ perNetwork, total } | null)`: a cap on questions from people who are not signed in; a visitor over it gets `{ ok: false, status: 429, error: 'visitor_limit', scope }` from `send()` ([Visitor limits](./sdk-builder.md#visitor-limits)). - A room named after this member opened it now gets its name (the name doc is read again on the settings' cadence). - Outside agents: a resumed lane's waiting drafts reach your `'a2a'` listener even when it is attached after `openLane` resolves (they wait for one); an empty assistant line is never taken for a draft. - In a vault: `wb.vault.info` follows the vault's sign-in and account (it was the welcome, frozen); "only who manages it changes" is decided with hidden members counted (it said "anyone else can no longer read" for a role change); a lane of an attested Space is refused with `attested`, as outside a vault. **2026-10-26** - **SDK 3.8.1 — the vault kit's headers add `Cross-Origin-Opener-Policy: same-origin`.** ⚑ **If you host a vault, add it** to every response of your vault's origin: a page that opens your vault in a window then gets no handle on it (the protection Witbitz's own vault has). Re-run the kit (`vault-kit.mjs`, 3.8.1) and `HEADERS.md` lists it for nginx, Caddy and Apache; your screens and `vault.js` are kept. Nothing else changes for your app. **2026-10-25** - **SDK 3.8.0 — outside agents' waiting drafts survive a reload, and the sync sweep's fixes; one change in a vault.** - `lane.snapshot()` carries the outside-agent tasks still waiting for a draft or for the member (`a2a`); resume the lane with it and each waiting draft comes back as an `'a2a'` event ([Outside agents](./sdk-agents.md#a2a)). Before, a reload lost them, and the agent's task stayed "working". - Outside agents: a job agent's report (`job: true`) is never drafted as a reply; a shared-room line mirrored into the lane is never taken for its assistant's draft; the agent's name is no longer kept as the member's name. - ⚑ **In a vault, `wb.backup.remove(room)` returns a promise** (it resolved nothing you could see; a refusal was swallowed) — `await` it. `wb.vault.openRoom(room)` refuses a room the vault does not hold (`not_in_vault`), and `JSON.stringify(wb)` shows the vault's sign-in. - In a vault: `changeMembers` / `rotateKey` wake a sleeping confidential Space first (it failed with `config_unavailable`); webhook and job connections keep `webhook` / `job`; a role-only change is said as one and emits no `rekeyed`; `invite.enroll()` resolves `'vault'`; `startMcp`'s sign-in comes back to your page's `/mcp-callback`. - Cloud computers: a Deny with a `message` falls back to the plain refusal on a connector from before messages (given the `session`); the published p2y image is accepted. **2026-10-24** - **SDK 3.7.0 — the person's cloud computers; nothing breaks.** `wb.computers`: `list`, `create`, `open`, `remove` and `usage`, and an open `Computer` with its sessions, approvals, questions, files and live events — the Witbitz app's Code section, from your page ([Cloud computers](./sdk-computers.md)). Kept sealed in your app's account; opening checks the published Witbitz image and follows a new one (`images`); its hours are charged to the person's AWS key — your app's own key under their account, with its own monthly cap. Not in vault mode yet (`vault_mode`). - **SDK 3.6.1 — a confidential lane of a standard Space sees its Space; nothing breaks.** `privateLane({ tier: 'enclave' })` on a standard Space made a lane whose assistant never saw the room: each turn named the standard room as its bridge, which the confidential tier cannot read, and nothing copied the room in. It now works as a lane of the other tier opened with `openLane`: your device mirrors the Space into it (without `mirror: true`), and its turns carry no bridge and not the Space's key ([Across tiers](./sdk-agents.md#lane-tiers)). A lane on its Space's own tier is unchanged. - **SDK 3.6.0 — mark an agent that takes jobs; nothing breaks.** `lane.a2a.addPeer(name, { job: true })` (and `.peers` shows `job: true`): that agent's messages are reports — its run has ended and it reads no reply — so the lane's assistant tells the member what came back and, if more is needed, proposes a new job with `connection_call` instead of answering the agent. Add your Claude Code routine's peer this way ([Outside agents](./outside-agents.md)). Also: when a confidential Space's enclave cannot be reached for a moment (it is being replaced or relaunched), `changeMembers` and `rotateKey` wake it and read again instead of failing with "only an email-gated Space"; a read that still fails is `config_unavailable` (retry). - **SDK 3.5.0 — outside agents over MCP, and agents that take jobs; nothing breaks.** `lane.a2a.mcpUrl` (also returned by `enable` and `addPeer`): the lane's door for agents that take a custom MCP server — Claude Code, Grok, Cursor — with the same per-peer token as A2A. `wb.connections.attach({ webhook: true, job })`: an agent started by a web address (a Claude Code routine's trigger) as a connection; a Space granted it tells the agent, in every job, to report back with `message_member`. See [Outside agents](./outside-agents.md). **2026-10-23** - **SDK 3.4.0 — MCP servers as connections; nothing breaks.** `wb.connections.startMcp`, `attachMcp`, `finishMcp` and `mcpTools`, and `space.connections.grant(conn, { tools, auto })` for an MCP connection ([MCP servers](./sdk-accounts.md#mcp-servers)): the server's sign-in runs inside the Sentinel, and the grant brings its recommended tools (reads on their own, changes ask) or the ones you choose. In a vault they run through it. - **SDK 3.3.0 — your tenant's origins, signed; nothing breaks if you do not.** - With a signing key set, `admin.origins.declare([...])` also files the same list, signed, with the Sentinel; `admin.signing.publishOrigins()` files it directly and `admin.signing.status()` shows the list held ([Signing your tenant's requests](./sdk-builder.md#tenant-signing)). From then on only your own pages can name your tenant there — a connection through your OAuth app, a user's erase list, an MCP or GitHub App sign-in. Until you file a list, nothing changes. - **SDK 3.2.0 — your tenant can sign its own requests; nothing breaks if you do not.** - **Tenant signing keys** ([Signing your tenant's requests](./sdk-builder.md#tenant-signing)): `new WitbitzAdmin({ …, signingKey })`, `WitbitzAdmin.generateSigningKey()`, `admin.signing.register()` and `admin.signing.status()`. Once you register a key, the Sentinel refuses any request for your tenant that your server did not sign — Witbitz's own servers included. Until you register one, your requests are taken on your tenant key as before. - **Your users' sign-in is checked inside the Sentinel**, and each of your users' connections, Siri inbox and agent lanes now runs in its own process there, with its own key. No API change. - **SDK 3.1.0 — nothing to change in your code; move from 3.0.0 when you can.** - **What the account used:** `wb.account.usage({ month })` returns the month's totals by payer and day, and every SDK turn now carries its receipt address ([What the account used](./sdk-accounts.md#account-usage)). In a vault, the vault holds the key. - **Link Spaces change in place too:** an open Space's members change without a new room, and each member you add gets a link of their own. A page whose link no longer opens the Space meets `pending` with `reason: 'link_changed'`, not `removed`. - **Member changes:** removing someone also removes their @username entries; one person added by email and by @username counts once; a role-only change keeps the key; a rotation skips removed members and only owners and admins may rotate; a third member makes a direct chat a group. - **In a vault:** a page cannot change a Space's owners or admins without the person (`changeMembers` asks them); the vault checks a member change is allowed before it asks; `wb.invite` says the vault keeps the key; errors name `wb.vault.signIn`; `wb.signOut()` refuses with `vault_signs_in`; `connections.detach(id, { spaces })` works; a call that holds a screen for the person waits 15 minutes; an open lane of a verified Space is refused (`lane_downgrade`). - **Outside agents:** an answer to an outside agent is the lane's own reply, never sent to the shared Space. **2026-10-22** - **SDK 3.0.0 — members change in place, and only in place; pin 3.0.0 if your users are in verified rooms.** A verified room's members (and its key) change without a new room ([Changing members in place](./sdk-people.md#members)): `space.changeMembers()`, `space.rotateKey()`, `wb.invite`, and the `rekeyed` / `removed` / `pending` events. ⚑ **Breaking:** `space.fork()` is retired — it throws `use_change_members` and makes nothing; `plan()` refuses a `history` with the same code and answers `in_place` for a new member list; `capabilities().features` no longer lists `fork`. A move an older client made is still followed ([A Space no longer moves](./sdk-people.md#fork)). ⚑ **A page on 2.2.x cannot follow a re-keyed room**: once a room is re-keyed (by your app on 3.0.0, or by a member in the Spaces app), every call a 2.2.x page makes with the old key is refused with `stale_key`. Move to `/sdk/3.0.0/`. Also: - **A new Witbitz Sentinel build is followed everywhere.** `wb.a2a`, `wb.erasure` and the admin client (`WitbitzAdmin`, `witbitz-admin.mjs`) attested with the release's pins alone, so 2.2.4's outside agents, erase list and provider calls failed once the Sentinel moved to a build after its pins. They follow the published claim now, as the rest of the SDK does (`sentinel: { follow: 'off' }` or your own `pins`: never). - **No private lane for an attested Space** — a Space whose owner policy requires an attested app, not a confidential Space: `privateLane()` throws `attested` (no app signs a lane its owner policy yet), as the Spaces app waits. - **Lanes linked across tiers:** `lane.link(otherLane)` takes a lane of either tier, as in the Spaces app. The rooms a lane reads (`setLinks`) stay on its own tier. - A Space made in a [vault](./sdk-keys.md#vault-mode) names its creator its owner, as outside a vault. **2026-10-18** - **SDK 2.2.4 — the admin client as one file for your server.** `witbitz-admin.mjs` in each release from 2.2.4: download it and import it from your own code ([Builder calls](./sdk-builder.md#builder-calls)). The import our docs showed before — `from 'https://witbitz-spaces.pages.dev/witbitz-admin.js'` — does not load in Node. Also: `admin.origins.declare` answers `unproven`, the origins whose host does not yet say they are yours ([prove an origin](./sdk-builder.md#prove-origin)); and this release knows the current Sentinel build (st20). - **SDK 2.2.3 — move to it before the Sentinel your release pins is retired.** A new standing-Sentinel build is now followed (`sentinel.follow`, default `'published'` — see [Sign in](./sdk-people.md#sign-in)); no earlier release can follow one, so their connections, Siri and outside agents stop when the build they pin is retired. Also (from the daily review): a private lane of a **verified** Space is gated to its member (it was an open room) — `sign_in_required` with nobody signed in, `lane_downgrade` for an `admission` that would open it; the signed-in creator on a verified Space's list is its owner, as in the app; `fork()` of a room with no owners but a creator key refuses `not_creator` (its members follow only a move the creator signed); `siri.collect()` no longer acks a capture the Space did not take; `connections.detach(id, { spaces })` also drops it from those Spaces; `approve()`/`deny()` treat a 200 that decided nothing as a refusal; a linked lane's ask waits until the other lane's whole history is read; the room's name is re-asked every 20 of the room's entries (not this handle's sends); `witbitz-admin.js` errors carry a `code`; a backup's email step passes the server's reason as the code (`update_required`). In a vault: a reclaimed worker is restarted (calls waiting on it fail `worker_restarted`), per-user billing (`useUserToken`) reaches the vault's turns, `lane.link()` is recorded on the account, a lane's Siri drops go into the lane, a confidential move is refused before the question with `vault_unsupported` on `moveRefused`, errors name `wb.vault.signIn`, `account.lastError` is an `Error`, speech-key limits say how long to wait, and `openLane` refuses a tier other than the lane's. **Rebuild your vault with the 2.2.3 kit** to get the vault's half. The examples on this page now load 2.2.3. **2026-10-17** - **SDK 2.2.2 — update if your page restores backups with `wb.backup`.** The email step of a protected restore now sends the key the server needs to check the address it actually mails (`gk`), and the server REQUIRES it: from today, `wb.backup` in SDK 2.2.1 or earlier fails at the email step (2.2.1 says `email_refused`; the server's reason is `update_required`). Load 2.2.2 (or `latest`). **In a vault, rebuild it with the 2.2.2 kit too**: a vault built with an earlier kit keeps the old backup code, whatever SDK the page loads. It also carries the fixes made in the app since 2.2.1. **2026-10-15** - **SDK 2.2.1** (fixes from the daily review; nothing to change in your code): in a vault, a private lane's `connections` and `siri` go through the vault as a Space's do; `space.plan()` judges a change as the vault's signed-in member; `turn.abort()` stops the turn on the server; a member's vault follows a move of a split-key Space, and both vaults keep it split; a vault whose `screens.js` came from 2.1.x shows the kit's own recovery-code, restore and Siri-key screens (`startVault` takes a `brand` for them). Everywhere: a stored answer's late stream frames are no longer replayed as a `partial`. Types: `VaultRoom.lane`/`tier`, `Connection.callKey` optional (none in a vault), `rotateKey()` may resolve `null`, `wb.vault.on('account')`. Rebuild a vault with the 2.2.1 kit to get its fixes. The examples on this page now load 2.2.1. - **SDK 2.2.0** (vault mode): everything the SDK offers now works in a vault — live streams; connections, usernames, the Siri inbox and outside agents through the vault's sign-in; private and linked lanes, `sealedProof()`; `fork()` and following a move; confidential, split-key and direct-chat Spaces; the backup, the account index and the erase list; dictation and reading aloud. What changes for a page already in a vault: those calls now work instead of refusing `vault_unsupported`; `wb.siri.shortcut.key` is `null` (`wb.vault.showSiriKey()` shows it in the vault); `wb.backup.enable()` resolves `{ enabled: true }` and `restore()` takes the code in the vault's screen; a Space's settings read carries no connection key; `openLane` takes the lane's room id. Rebuild your vault with the 2.2.0 kit (it replaces `core/` only). A `screens.js` kept from 2.1.x has no recovery-code, restore or Siri-key screen: the vault shows the kit's own for those (from 2.2.1; in 2.2.0 those calls refused `no_screen`) until you add yours. Outside a vault: a confidential `fork()` no longer fails when the enclave already holds the new Space's settings, and a Space with no admins follows only a move its creator key signed (the app's rule). The examples on this page now load 2.2.0. - **SDK 2.1.2** (vault mode only): seventeen more members that half-worked in a vault now refuse by name — the backup's and the account index's getters and methods, `space.connections` / `space.siri`, and `wb.dictation()` / `wb.speak()` / `wb.speech` without a user token. A test now walks every public SDK member in a vault, so a new one cannot ship undecided. The examples on this page now load 2.1.2. Outside a vault nothing changed. - **SDK 2.1.1** (vault mode only): in a vault, what is not available yet now refuses by name (`vault_unsupported`, `vault_signs_in`) instead of half-working — 2.1.0's backup handed out a recovery code that recorded nothing, its connections / usernames / Siri said "sign in first" while signed in, and `sealedProof()` threw a TypeError. The examples on this page now load 2.1.1. Outside a vault nothing changed. - **SDK 2.1.0** ([Versions](./sdk.md#versions)): the examples on this page now load `/sdk/2.1.0/witbitz-sdk.js`. It carries the 2026-10-13 changes below (error codes on every error, `plan()` answering a refused gate instead of throwing). It adds [vault mode](./sdk-keys.md#vault-mode) — `Witbitz.inVault({ vault })`, `wb.vault`, and the kit tool (`/sdk/2.1.0/vault-kit.mjs`): your app on one origin, the room keys in a vault you host on another — plus what the 2.0.0 entry below calls "in the next release": direct chats and `wb.speak()`. **2026-10-13** - **SDK 2.0.0** ([Versions](./sdk.md#versions)). The examples on this page now load `/sdk/2.0.0/witbitz-sdk.js`; 1.1.0 and 1.0.0 are unchanged. It is a new **major** version because it carries the breaking changes of 2026-10-08 to 2026-10-11 below — before you move from 1.1.0, check those entries marked *Changed* and *Removed* (the ones most likely to touch your code: `configure()` refusing fields it used to drop, `admission.approve: 'admins'` and `admission.removeUsers` refused, `admission()` without `usernames` / `approve`, no `revoked` reason, `rename()` / `approve()` / `deny()` rejecting when there was no answer, actions signed by the member's name instead of `"You"`). New over 1.1.0: `space.run()`, builder mode, `space.title` / `rename()`, `react` / `replyTo`, `usage` on a turn, `isLaneCopy`, signed webhooks in `connections.attach`, registered MCP apps (`admin.providers.attachMcp`), and the confidential-room key healing. - New: [direct chats](./sdk-people.md#direct-chats) — `createSpace({ direct: email })` and `space.directPeer()`, the Spaces app's one-to-one chat (live build; in the next release). - New: [`wb.speak(text)`](./sdk-assistant.md#speak) — text read aloud with the Spaces app's Play aloud player (live build; in the next release). - Changed: every error the SDK throws now carries a `code` ([Errors](./sdk-reference.md#errors)); an argument a call cannot use is `invalid_argument`, a link that is not a Space link `invalid_link`, and a poll error with no code of its own `unavailable`. Messages are unchanged. - Changed: `plan()` returns `{ how: 'refused', code: 'invalid_admission' }` (or `removed`) for a gate `fork()` would refuse, as documented; it used to throw. `fork()`'s own error for such a gate now carries `code: 'invalid_admission'`. **2026-10-11** - Changed: `configure()` refuses what it cannot change in place, before sending anything — `create_time` (`tools`, `immediateTools`, `tier`, `key`, `agent.tools`), `use_fork` (`admission`; `lane` on a lane), `unknown_field` (anything else). It used to drop them without a word. - Changed: `rename()`'s message is the app's (`renameRefusal.js`), and a gated room's refusal carries `e.reason`. - Changed: `connections.revoke` and the Siri record throw the server's code (or `failed` / `refused`), not `no_answer`, when the server answered. - Changed: `tell_lane` refuses while the asking lane's history is still loading (the hop it continues is not known yet). - Changed: `approve()` / `deny()` reject when the decision got NO answer (status 0 — the network, "Hide my IP"); they used to resolve as decided. Such a decision may still have reached the room: `pending()` says whether the card is still waiting. - Changed: an @username is only a redirect. Inviting one stores a hash of that account's email, never the username; `admission.removeUsers` is refused (`code: 'removed'`) and `admission()` no longer returns `usernames` (a legacy room's @username entries count in `hidden`). - Changed: `lane.link(other)` links lanes of different tiers (it threw `tier_mismatch`). - Removed: a room's `revoked` list. Removing someone is a move to a room that does not list them (`space.fork()`); an existing room's list is ignored, and `not_authorized` no longer carries `reason: 'revoked'`. - Changed: a confidential room no longer carries an acceptance block. Which enclave build is accepted is each person's own choice in the Spaces app (kept on their account); the SDK seals a room key to any build Witbitz publishes. - Removed: `admission.approve: 'admins'` (every card admin-only). Creating or moving a room with it is refused (`code: 'removed'`), and an existing room's setting is ignored: any member decides an ordinary card. Admins still decide the cards that write an admin-only collection or call a connection. `admission()` no longer returns `approve`. - Changed: `rename()` throws `unavailable` when the room did not answer or failed (status 0, 5xx); it used to write the shared doc and resolve `ok: false`, as if an older server had taken the name. **2026-10-10** - Changed (TypeScript): `Message.spk` is typed as the public key it always was (`{ kty, crv, x, y }`), not `string`. Compare `x`/`y`; see [Messages](./sdk-rooms.md#messages) for "is this mine". - New: in a confidential room, a key the enclave no longer holds (`bad_mkseal`, after the enclave was replaced) or none at all (the room was opened while its enclave slept) is healed when the enclave answers: the SDK attests it and sends the call once more — a send, a reaction, a row, an approval, a rename, a run. If the enclave cannot be reached or verified, the call fails as before: `bad_mkseal`, or `code: 'enclave_unavailable'` (try again shortly). - Changed: after a refusal no retry cures (`not_on_list`, `revoked`, `gate_unreadable`) the poll and the proposals check read once a minute instead of nonstop; a failed proposals read keeps the cards you have (it used to empty them). - New: `isLaneCopy(m)` — the app's own test for a lane's copy of its shared room's line; filter on it, not `source`. - Changed: a collection declared `write: 'admins'` in a room with no admins or owners is refused at creation (`admins_required`) instead of accepted and never enforced. - Changed: who an action is signed by. `approve`, `deny`, `react`, `rename`, `configure`, row writes and connection grants used to default to `"You"` — which other members then read ("You renamed…"). With no `from`/`by`, they now use the name this member last sent or acted under, else `"A member"`. Pass `from` to choose. `send()` still defaults to `"You"`. A lane's crossing is signed the same way. - New: every turn (and `run`) carries the room's model pick from the Spaces app's model chip, and a handle re-reads the room's reply mode and model when they are 20 s old; [`space.react`](./sdk-rooms.md#replies) and `send(…, { replyTo })`; a room used only through the SDK is named after its first exchange; `sealedProof()` works in a verified room (read with the key). - New: [`space.run()`](./sdk-assistant.md#run) — the assistant as a function: one answer, optionally structured by a `schema`, and nothing written to the room. Standard rooms only for now; paid by your user's or your app's key, 4 at once and 60 a minute per room. - New: [builder mode](./sdk-assistant.md#builder-mode) — `framing: 'builder'` (your instructions and tools only, no Spaces framing), `dataWrites: false` (`data_query` without the write tools) and `send(…, { showPrompt: true })` (the exact prompt a turn sent). Nothing changes for a room that sets none of them. - New: [your app's name](./sdk-builder.md#app-name) in the sign-in mail — "Your Keepwarden code", from "Keepwarden via Witbitz" — for a sign-in on one of your declared origins; `admin.app.rename(name)` sets it. - New: `usage` on `send()`'s answer and on `space.turn()`'s `finished` — tokens in, out and cached, and the provider's cost ([What a turn used](./sdk-assistant.md#turn-usage)). - A refused builder call (`wb.users.erase`, `wb.origins…`) now carries the server's reason: `e.code` is the server's error code and `e.body` its answer. A bare 503 from the gateway is `e.code === 'gateway_timeout'` — the call ran past 30 s and may have finished; it is safe to call again. `users.erase` now stops inside that limit and answers `incomplete: true` when there is more to do. **2026-10-08** - New: `space.title`, `space.rename()` and the `title` event ([The room's name](./sdk-rooms.md#room-name)). In the next release; `/sdk/1.1.0/` is unchanged. A room's name entries never appear in `messages`. **2026-10-07** - **SDK 1.1.0** ([Versions](./sdk.md#versions)). The examples on this page now load `/sdk/1.1.0/witbitz-sdk.js`; 1.0.0 is unchanged. Over 1.0.0, 1.1.0 carries everything below and under 2026-10-05 that 1.0.0 did not: row schemas, sign-ins scoped to your app, two devices syncing an account at once, `wb.protect`, and types that match what the code returns. - **`send()` and `Lane.send()` results always carry a boolean `ok`.** It is `false` on a refusal such as `user_token_required`, which used to come back as `{ ok: true, error }`, and `true` on a reply, a silent turn, or a turn still running. Code that checks `error` keeps working. Row writes answer `{ ok: true, id, ops }`. - New: `wb.erasure` — the rooms the SDK creates can be deleted when you erase a user ([Erasing a user who has left](./sdk-builder.md#user-erasure)). - New: `platformTools` on `createSpace`, `privateLane` and `configure` ([Choosing the platform tools](./sdk-assistant.md#platform-tools)). - New: restricted keys, `admin.keys` ([Restricted keys](./sdk-builder.md#restricted-keys)); `admin.rooms.list()` and `admin.rooms.idleDeletion()` ([Your rooms, and the idle rule](./sdk-builder.md#room-list)); `wb.protect` ([Protect](./sdk-accounts.md#protect)). - A moved room keeps its verified marks: rows carry `_carried`, messages `carried` and `carriedBy` ([fork](./sdk-people.md#fork)). - Email-gated rooms stream live like open rooms ([Live updates](./sdk-rooms.md#live)). No code change. **2026-10-05** - **Sign-ins made on your website are scoped to your app** ([Sign in](./sdk-people.md#sign-in)). Connections, Siri, A2A and dictation made from your page now live under your app's own account for that user — connections or Siri set up **before** this change under the plain email are not visible to new sign-ins on your page. `wb.usernames.claim()` / `.release()` from your page answer `first_party_required` (the @username is the user's global name; `.mine()` and `.resolve()` still work). Room admission is unchanged. - New: [row schemas](./sdk-store.md#schemas) — a collection's `schema` is enforced on every write, yours and the assistant's (`invalid_row`); a schema with a rule we cannot check is refused at declaration (`invalid_schema`). - New: `space.plan(change)` — what a change requires (in place, a move, or a new room) and whether the history comes along, without making it. New: `wb.capabilities()` and the `SDK_VERSION` export. - **This page is reorganised as a [learning path](./sdk.md#path)**: quickstart → build an app → the AI's authority → people → personal AI → production → the security model → builder operations; the full call table is [the surface](./sdk-reference.md#the-surface), near the end. Every section and link kept its anchor. - **SDK 1.0.0, the first versioned release** ([Versions](./sdk.md#versions)). The examples on this page now load `/sdk/1.0.0/witbitz-sdk.js`. `/witbitz-sdk.js` still serves the live build, as before. - New: `space.turn(text)` — a handle on one turn's answer ([A turn you can hold](./sdk-assistant.md#turn)). - New: `space.on('message', cb, { replay: false })` hears only what arrives after the room's history was read. The default (history replays into the callback) is unchanged. - `on()` now returns a function that removes the listener. It still chains (`space.on(…).on(…)` works as before), but it is no longer the `Space` itself: code that kept `on()`'s return value as the space should keep the space instead. - New: `WitbitzAdmin` in `witbitz-admin.js`, the server-side client for builder calls ([Builder calls](./sdk-builder.md#builder-calls)). `wb.users` is `admin.customers` there. Nothing is removed: the page client's `wb.origins`, `wb.users`, `wb.billing`, `wb.rooms`, `wb.hosts`, `wb.providers` and `wb.verifyTenantKey()` delegate to it, and a page that passes `tenantKey` to `new Witbitz` now logs a console warning. - A `Space`, `Lane` and the `Witbitz` client no longer show their key, link or tokens when logged or serialised: `console.log(space)`, `JSON.stringify(space)` and error reporters print `{ room, shareLink: '[REDACTED]', … }`. The properties that hold them are no longer enumerable, so `Object.keys(space)` and `{ ...space }` no longer list `_mk`, `shareLink`, `_client` or `_wb`. Read them by name as before: `space.key`, `space.shareLink`, `wb.session`. - `lane.a2a.addPeer` refuses a 33rd agent on one lane (`too_many`), as the Spaces app always has. - A private lane created without `agent.instructions` now uses the Spaces app's lane instructions (a fuller version of the old default: it also tells the assistant never to paste a message meant for the room as a normal reply). - New: `space.on('update', ({ id, message, fields }) => …)` — a message you already have changed in place (a reaction, an image caption, a reply's cards), and `space.messages` holds the new version where the old one stood. Before, these were never seen. A mirrored copy of a message (`sh::`) is now the same message as its original. - **Docs corrected (no code change):** `wb.openSpace()` never read the page's URL on its own, whatever this page said — pass `location.href` ([Invite links](./sdk.md#invite-links)). `space.deny(id, { from })` takes `from`, like `approve`. `self: true` marks the agent's output, never your own message ([Messages](./sdk-rooms.md#messages)). Creating rooms needs a tenant id and a declared origin ([Do I need a tenant?](./sdk.md#tenant)). New: [the proposal round trip](./sdk-approvals.md#proposal-round-trip), [error codes](./sdk-reference.md#errors) and [types](https://witbitz-spaces.pages.dev/witbitz-sdk.d.ts). - `wb.backup.enable()` now writes the account format the Spaces app uses. A backup you already have keeps its format (`wb.backup.format === 'legacy'`) through every restore and save; `await wb.backup.upgrade()` moves it over (same code, same rooms — a client on an older SDK can then no longer restore it). Each app still has its own vault: a restore never opens a Witbitz account's backup, and the Spaces app never imports yours. - New: `wb.refreshSession()` renews a signed-in session without a new code ([Sign in](./sdk-people.md#sign-in)). - `signIn` errors now also carry `code`, `reason` and a readable `hint`; their messages are unchanged. - A device action (`mode: 'await'`) that throws now answers the turn with `"The tool failed on this device: "` instead of the bare message. - `partial`: a status with no words yet (`{ text: '', step }` — "Deciding whether to reply…", "Working on it…") now ends with `{ text: null }` when the turn ends without a reply. Before, it could stay on screen. Handle `text: null` as "nothing in progress" (you already do if you clear the bubble after a reply). - `partial`: a status that could not be opened is ignored; before, it could arrive as an unreadable `step`. - New, nothing to change: `space.pending()` and the `proposals` event (everything waiting, including proposals decided on another device), `deny(id, { from })`. **2026-10-02 (second release)** - `data.put` / `batch` refuse a row with an `id` field or a field starting with `_` (`reserved_field`). Pass the row's key as `{ id }`. Rows are read without such fields. - `space.connections.grant` refuses a Space that isn't email-gated (`gate_required`). - A lane's `mirror: true` no longer copies the shared room's row writes; pass `mirror: { data: true }` to keep them. - In a gated room, `admission()`, `admits()` and `isAdmin()` throw `sign_in_required` before the member signs in. - Fixed: `admins` and `approve: 'admins'` now reach the room; before this release they were dropped at creation. **2026-10-02** - `approve()` / `deny()` throw when the room refuses (`not_admin`, `not_authorized`, …) and keep the card; before, the card was dropped and `resolved` fired.