From 6811c5ed44477ea8743434db2f0417d628988e50 Mon Sep 17 00:00:00 2001
From: August Cayzer
Date: Sun, 25 Oct 2026 18:54:09 +0000
Subject: [PATCH 1/3] docs(swift-sdk): describe removing a copy
---
content/docs/(reference)/swift-sdk.mdx | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/content/docs/(reference)/swift-sdk.mdx b/content/docs/(reference)/swift-sdk.mdx
index 444b85af..0e2e80a1 100644
--- a/content/docs/(reference)/swift-sdk.mdx
+++ b/content/docs/(reference)/swift-sdk.mdx
@@ -50,6 +50,7 @@ let copy = try await WorkingCopy.open(store: storeURL, server: Server(url: serve
| --- | --- |
| `WorkingCopy.open(store:server:fileBudget:)` | Opens the file. Without a `Server`, the copy works offline only. A `fileBudget` sets the [file budget](#keep-files-on-the-device) as the copy opens, and a reader refuses it with `readingHandle`. |
| `WorkingCopy.replaceRefused(store:server:fileBudget:)` | Replaces a store that `open` refuses with `wrongSchema` by an empty one, and opens it. [Replace a refused copy](#replace-a-refused-copy) says what it keeps. |
+| `WorkingCopy.remove(store:unsent:)` | Removes a store, once the person has agreed to lose the writes it holds that the instance has not taken. [Remove a copy](#remove-a-copy) says when it refuses. |
| `WorkingCopy.openReader(store:)` | Opens a store another process writes, to read only. It refuses a path where no store exists. |
| `handle` | `.writer` or `.reader`. A reader refuses writes, hydration and catch-up with `readingHandle`. |
| `useKey(_:)` | Gives the open copy a new credential. Held `changes()` streams go on. |
@@ -67,6 +68,19 @@ A store that another build of the core made throws `wrongSchema` from `open`, an
- **What only the old store held is gone:** pins, the position in the instance's events and declared types.
- **A store that holds an unsent write is left as it is**, and the call throws `wrongSchema` with the count. A store that opens as it is, or a path with no store, throws `invalid`. [Troubleshooting](/run/troubleshooting#staying-in-step) covers what to do with unsent writes.
+### Remove a copy
+
+`WorkingCopy.remove(store:unsent:)` removes the file, its logs, the files the copy downloaded and its lock, and nothing else beside them. It removes a store of any build, including one that `open` refuses with `wrongSchema`. A symbolic link at one of those names is removed, never what it leads to.
+
+Show the person the store's `unsent` count, which is the number of writes the instance has not taken, and pass the count they agreed to lose as `unsent`. The call holds the store's writer lock throughout, so no write arrives between the check and the removal. It removes the store only when the count matches. Otherwise it leaves everything as it is.
+
+- **A different count throws `unsentDiffers`.** Its `unsent` is the store's own count, or `nil` when the core can't read one, and a store like that is never removed. Its `agreed` is the number you passed. Show the person the store's count and ask again.
+- **A store that something else writes throws `readingHandle`.** That includes another process and another open copy in your own app, so close your copy first.
+- **A folder's store throws `invalid`.** It goes only with the folder.
+- **A path with no store** is removed already. The call takes away what an earlier removal left there when `unsent` is `0`.
+
+A helper that only reads the store, such as one you opened with `openReader(store:)`, doesn't stop the removal and keeps showing the old copy until it reopens. [Devices](/devices#the-working-copy) explains the removal for every binding.
+
## Reach a plain http server
An `http` address sends the key in the clear. **The SDK warns about that only through your app**: it shows nothing itself. `Server.isPlainHTTPToAnotherHost` is `true` for an `http` address on any host but the machine itself, by the same rule as the command line. `localhost`, every address in `127.0.0.0/8` and `::1` are the machine itself; an IPv4-mapped address such as `::ffff:127.0.0.1` is not. `takePlainHTTPNotice()` returns a `PlainHTTPNotice` the first time any server with that origin asks in the process, and `nil` after, for `https` and for the machine itself:
@@ -515,7 +529,7 @@ Every call on a working copy, `Folders` or `Client` throws a `MarfaError`, whose
| `noConnection` | A `Client` call found the device offline, so nothing was sent. |
| `contractMismatch` | The server speaks a contract this build wasn't made for. A write it answered stays queued, and goes again once the app speaks the server's contract. |
| `wrongServer` | The server at the copy's address is another instance than the copy recorded. The copy sends it no credential or data: [Troubleshooting](/run/troubleshooting) explains. |
-| `copyExpired`, `hydrationIncomplete`, `readingHandle`, `firstSyncWaiting`, `noServer`, `closed` | States covered on this page. |
+| `copyExpired`, `hydrationIncomplete`, `readingHandle`, `unsentDiffers`, `firstSyncWaiting`, `noServer`, `closed` | States covered on this page. |
The other cases (`unknownType`, `io`, `decoding`, `store`, `storageFull`, `signedOut`, `noKeychain`, `redirected`, `noCursor`, `noCatalog`, `wrongSchema`, `bytesAbsent`, `streamIncomplete`, `canceled` and `invalid`) each carry a message that says what happened. The [marfa-swift repository](https://github.com/withmarfa/marfa-swift) holds the source.
From 1db0941d886ac25bff0f3a6ab712cda4aeb7eaa4 Mon Sep 17 00:00:00 2001
From: August Cayzer
Date: Sun, 25 Oct 2026 19:18:07 +0000
Subject: [PATCH 2/3] docs(swift-sdk): say what a missing store answers
---
content/docs/(reference)/swift-sdk.mdx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/content/docs/(reference)/swift-sdk.mdx b/content/docs/(reference)/swift-sdk.mdx
index 0e2e80a1..928b00ba 100644
--- a/content/docs/(reference)/swift-sdk.mdx
+++ b/content/docs/(reference)/swift-sdk.mdx
@@ -77,9 +77,9 @@ Show the person the store's `unsent` count, which is the number of writes the in
- **A different count throws `unsentDiffers`.** Its `unsent` is the store's own count, or `nil` when the core can't read one, and a store like that is never removed. Its `agreed` is the number you passed. Show the person the store's count and ask again.
- **A store that something else writes throws `readingHandle`.** That includes another process and another open copy in your own app, so close your copy first.
- **A folder's store throws `invalid`.** It goes only with the folder.
-- **A path with no store** is removed already. The call takes away what an earlier removal left there when `unsent` is `0`.
+- **A path with no store** is removed already. The call takes away what an earlier removal left there when `unsent` is `0`, and throws `unsentDiffers` with a count of `0` otherwise.
-A helper that only reads the store, such as one you opened with `openReader(store:)`, doesn't stop the removal and keeps showing the old copy until it reopens. [Devices](/devices#the-working-copy) explains the removal for every binding.
+A helper that only reads the store, such as one you opened with `openReader(store:)`, doesn't stop the removal and keeps showing the old copy until it reopens. [Devices](/devices#the-working-copy) explains the removal.
## Reach a plain http server
From 8c129fe9095bbc647be2471d2c413b887e520180 Mon Sep 17 00:00:00 2001
From: August Cayzer
Date: Sun, 25 Oct 2026 20:17:18 +0000
Subject: [PATCH 3/3] docs(swift-sdk): add the v0.0.34 changelog entry and name
the Swift removal call
---
content/docs/(overview)/devices.mdx | 2 +-
content/docs/(reference)/changelog.mdx | 7 +++++++
content/docs/run/troubleshooting.mdx | 2 +-
3 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/content/docs/(overview)/devices.mdx b/content/docs/(overview)/devices.mdx
index 30feecbb..1464ac37 100644
--- a/content/docs/(overview)/devices.mdx
+++ b/content/docs/(overview)/devices.mdx
@@ -39,7 +39,7 @@ The hydration **empties the copy first** if the instance's event log now ends be
A copy made by an earlier build of the core can have a different shape. The device **refuses to open it** with `wrong_schema`, deletes nothing, and says how many writes the file holds that the server hasn't taken, because only the build that made it can send them. If some are of a kind the current build no longer sends, it names the kind. [Troubleshooting](/run/troubleshooting#staying-in-step) says what to do. An app built on the core's Node or Swift binding can replace a refused copy that holds nothing unsent with an empty one; the files the copy downloaded and the file budget's setting stay. A [folder](/run/sync-a-folder#move-to-a-newer-cli) can replace its own copy itself.
-**An app can remove its working copy, with the unsent writes the person agreed to lose.** An app passes the number of writes the server hasn't taken that it showed the person, through the core's Node binding (`MarfaCore.removeStore`). The device removes the copy only when the copy holds exactly that number, whatever build made it: the file, its logs, the files it downloaded and its lock, and nothing else beside them. A symbolic link at one of those names is removed, never what it leads to. If the copy holds another number, or one the device can't read, the device refuses with `unsent_differs`, says the copy's own number, or that it can't read one, and leaves the copy, its logs, its downloaded files and its lock as they were. It refuses with `reading_handle` while anything else writes the copy, the app's own open copy included, so the app closes its copy first. A helper process that only reads the copy doesn't stop the removal, and keeps showing the old copy until it reopens. A folder's copy goes only with the folder, so the device refuses it with `invalid`. A path with no copy holds no unsent write, so removing it again finishes a removal that stopped part way.
+**An app can remove its working copy, with the unsent writes the person agreed to lose.** An app passes the number of writes the server hasn't taken that it showed the person, through `WorkingCopy.remove(store:unsent:)` in the Swift SDK or the core's Node binding (`MarfaCore.removeStore`). The device removes the copy only when the copy holds exactly that number, whatever build made it: the file, its logs, the files it downloaded and its lock, and nothing else beside them. A symbolic link at one of those names is removed, never what it leads to. If the copy holds another number, or one the device can't read, the device refuses with `unsent_differs`, says the copy's own number, or that it can't read one, and leaves the copy, its logs, its downloaded files and its lock as they were. It refuses with `reading_handle` while anything else writes the copy, the app's own open copy included, so the app closes its copy first. A helper process that only reads the copy doesn't stop the removal, and keeps showing the old copy until it reopens. A folder's copy goes only with the folder, so the device refuses it with `invalid`. A path with no copy holds no unsent write, so removing it again finishes a removal that stopped part way.
## A write made offline
diff --git a/content/docs/(reference)/changelog.mdx b/content/docs/(reference)/changelog.mdx
index f5ce3dc1..42e7d1db 100644
--- a/content/docs/(reference)/changelog.mdx
+++ b/content/docs/(reference)/changelog.mdx
@@ -801,6 +801,13 @@ The first release: the server, the CLI for macOS on Apple silicon, and the TypeS
The [Swift SDK](/swift-sdk) is released from its own repository, and each release carries a build of Marfa's core.
+### Swift SDK v0.0.34
+
+Released October 25, 2026. [v0.0.34 on GitHub](https://github.com/withmarfa/marfa-swift/releases/tag/v0.0.34)
+
+- **`WorkingCopy.remove(store:unsent:)` is new, with `MarfaError.unsentDiffers`.** An app removes a store, whichever build made it, once the person agrees to lose the unsent writes it holds. The call removes the store only when its count of unsent writes equals `unsent`, and otherwise throws `unsentDiffers` with the store's own count. An exhaustive `switch` over `MarfaError` needs the case. [Remove a copy](/swift-sdk#remove-a-copy) explains it.
+- **`replaceRefused(store:server:fileBudget:)` keeps the old store's file budget when `fileBudget` is `nil`.** [Replace a refused copy](/swift-sdk#replace-a-refused-copy) says what else it keeps.
+
### Swift SDK v0.0.33
Released October 25, 2026. [v0.0.33 on GitHub](https://github.com/withmarfa/marfa-swift/releases/tag/v0.0.33)
diff --git a/content/docs/run/troubleshooting.mdx b/content/docs/run/troubleshooting.mdx
index 60f74cf9..190c7044 100644
--- a/content/docs/run/troubleshooting.mdx
+++ b/content/docs/run/troubleshooting.mdx
@@ -281,7 +281,7 @@ The message says how many writes the file holds that the server has not taken: w
- **None.** Every write the file holds reached the server. Run `marfa device hydrate` to build a new working copy. An app built on the core's Node binding can call `replaceRefused` instead, and one built on the [Swift SDK](/swift-sdk#replace-a-refused-copy) can call `WorkingCopy.replaceRefused(store:server:fileBudget:)`: it replaces the old file with an empty copy, so what only the old file held, such as pins, is lost. The files the copy downloaded and the file budget's setting stay.
- **Some.** Open the file with the build that made it, run `marfa device drain` to send the writes, and use `marfa device discard` on any it refuses that you don't want. Then hydrate a new copy with the newer build. Keep the old file until you have done this, because deleting it loses those writes.
-An app that lets a person discard the copy instead can remove it through the core's Node binding (`MarfaCore.removeStore`), passing the count the person agreed to lose. The core removes the file, its logs, its downloaded files and its lock only if the file still holds exactly that count, and otherwise refuses with `unsent_differs` and the file's own count. [Devices](/devices#the-working-copy) explains the removal.
+An app that lets a person discard the copy instead can remove it with `WorkingCopy.remove(store:unsent:)` in the Swift SDK or the core's Node binding (`MarfaCore.removeStore`), passing the count the person agreed to lose. The core removes the file, its logs, its downloaded files and its lock only if the file still holds exactly that count, and otherwise refuses with `unsent_differs` and the file's own count. [Devices](/devices#the-working-copy) explains the removal.
**A folder replaces its own store.** Each folder keeps a working copy of its own, with its record of which file is which item, which files went missing, which writes and placements the server refused and which notes were purged. A folder command that writes, such as `marfa folders push`, `marfa folders pull`, `marfa folders scan` or `marfa folders watch`, handles a store from another build itself: