From 487f672d99f18b059fa10b48ae0316423e5a997d Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Sun, 25 Oct 2026 01:39:18 +0100 Subject: [PATCH 1/5] docs(devices): describe fills only in place and writes held for another instance --- content/docs/(overview)/devices.mdx | 19 +++++++++++++++++-- content/docs/run/troubleshooting.mdx | 11 ++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/content/docs/(overview)/devices.mdx b/content/docs/(overview)/devices.mdx index 108f8fd6..5a52a370 100644 --- a/content/docs/(overview)/devices.mdx +++ b/content/docs/(overview)/devices.mdx @@ -37,6 +37,20 @@ The hydration **empties the copy first** if the instance's event log now ends be **For any other reason, the copy refuses reads and writes until a hydration, which empties it first.** Examples are another instance answering at the same address, or a credential whose reach changed. A device's own change of an item's type or move of an edge's source, once the server takes it, also changes the reach: the device sends the writes queued behind it, then expires the copy once. +**An app that refills a stale copy by itself can ask for a fill only in place.** Such a hydration never empties the copy. Where the fill can't be in place, it's refused `copy_expired` before it changes anything, and the copy, its queued writes and its files stay as they were. The reason says why: + +| Reason | Why the fill can't be in place | +| --- | --- | +| `instance_changed` | Another instance now answers at the copy's address. | +| `reach_changed` | What the credential may read has changed. | +| `slice_changed` | The types, tier or edge types asked for aren't the ones the copy holds. | +| `head_past_log` | The instance's event log ends before the point the copy had reached, as after a restore from a backup. | +| `not_stale` | The copy isn't waiting to be filled again, for example because it's whole. | + +The app can then ask the person whether to build the copy again, which empties it, or keep it as it is. The CLI's `marfa device hydrate --in-place-only` does the same. + +**Writes queued against one instance aren't sent to another.** When a hydration builds the copy from another instance than the one it was built from, each write still waiting is `blocked` with the reason `instance_changed`, because the instance now at the address may never have held the items the write was made against. A release sends it to that instance; a withdrawal takes it out of the queue. + 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 `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. @@ -72,7 +86,7 @@ Each write the device settles gets one verdict: Only `refused`, `blocked` and `dead` need an **app's attention**. A write with no answer yet has no verdict. -A `blocked` write carries one of six reasons. Five hold it until something changes: +A `blocked` write carries one of seven reasons. Six hold it until something changes: | Reason | What holds the write | What clears it | | --- | --- | --- | @@ -81,10 +95,11 @@ A `blocked` write carries one of six reasons. Five hold it until something chang | `ancestor_unavailable` | The instance no longer holds the version the write was based on. | A withdrawal, or a release once the credential has come to read that version. Sent again before then, the write is refused the same way. | | `conflict_unresolved` | A newer change collided with the write, and the instance didn't settle the two. | A withdrawal. Sent again, the write is refused the same way. | | `failing_alone` | The write keeps failing as if the server were down, while the writes behind it go through. | A release or a withdrawal. | +| `instance_changed` | The write was queued while the copy came from another instance than the one it's now built from. | A release, which sends it to the instance now at the address, or a withdrawal. | A **release** sends the write again in the next drain, under a fresh idempotency key. A **withdrawal** takes the write out of the queue and puts the copy back to what the instance holds. Withdrawing a create, or a write blocked `failing_alone`, also refuses the writes that wait on it. The Swift SDK's [queue](/swift-sdk#send-writes-and-read-verdicts) and the CLI's `marfa device release` and `marfa device withdraw` do both. -The sixth reason, `awaiting_dependency`, marks a write waiting behind another that has no answer yet, such as an edit of a note whose create hasn't been answered. Nothing needs to be done: the Swift SDK shows it as waiting, and the answer to the write it waits for releases it. +The seventh reason, `awaiting_dependency`, marks a write waiting behind another that has no answer yet, such as an edit of a note whose create hasn't been answered. Nothing needs to be done: the Swift SDK shows it as waiting, and the answer to the write it waits for releases it. ## Folders diff --git a/content/docs/run/troubleshooting.mdx b/content/docs/run/troubleshooting.mdx index 4e5d14bd..d8411b6c 100644 --- a/content/docs/run/troubleshooting.mdx +++ b/content/docs/run/troubleshooting.mdx @@ -271,14 +271,23 @@ Build the working copy again. [Devices](/devices) explains when a copy has to st -This is how the CLI's device commands report `catchup_too_old` and `reach_changed`, with the code `copy_expired` and exit code `4`. Writes waiting in the queue are kept. A device that sends a change of an item's type, or a move of an edge's source, that the server takes sends the writes queued behind it and then reports `reach_changed`, once. If the drain also meets a wait or an outage, it returns that report first, and the next call reports `reach_changed`. +This is how the CLI's device commands report `catchup_too_old`, `reach_changed` and `instance_changed`, with the code `copy_expired` and exit code `4`. Writes waiting in the queue are kept. A device that sends a change of an item's type, or a move of an edge's source, that the server takes sends the writes queued behind it and then reports `reach_changed`, once. If the drain also meets a wait or an outage, it returns that report first, and the next call reports `reach_changed`. Run `marfa device hydrate` again. After `catchup_too_old`, `marfa device status` reports the copy's hydration as `stale`, and the copy keeps answering reads, marked `stale`, while the hydrate fills it in place. Until the hydrate finishes, `marfa device drain` and `marfa device catch-up` keep answering `copy_expired` with `catchup_too_old`. If the credential no longer reads one of the types you held, leave that type out of `--types`, or the hydrate is refused with `type_not_permitted`. An app built on the Swift SDK that holds a `changes()` stream refills a stale copy by itself. [Refill a stale copy](/swift-sdk#refill-a-stale-copy) says how. +After `instance_changed`, the hydrate builds the copy from the instance now at the address and holds each write still waiting as `blocked` with the reason `instance_changed`, so none reaches that instance by itself. Check `marfa device queue`, then send the ones you want there with `marfa device release --reason instance_changed`, or take one out with `marfa device withdraw `. + +`marfa device hydrate --in-place-only` gives the same error, with the reason `instance_changed`, `reach_changed`, `slice_changed` or `head_past_log`, where the copy can't be filled again in place. It changes nothing. Run `marfa device hydrate` without the option to build the copy again. + For a folder, run `marfa folders push` again instead: it builds the copy again itself. [Sync a folder](/run/sync-a-folder#replace-a-folders-key) covers a key you replaced. + + + +`marfa device hydrate --in-place-only` fills only a copy whose cursor the instance's event log let go, and this copy is whole, was never built, or has to be built again. Nothing was changed. Run `marfa device catch-up` to bring a whole copy up to date, or `marfa device hydrate` without the option to build it again. + From 0481c37293c5343f08842ade5c55a1ab4efee166 Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Fri, 30 Oct 2026 22:21:19 +0000 Subject: [PATCH 2/5] docs(devices): say an instance other than, not another instance than --- content/docs/(overview)/devices.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/docs/(overview)/devices.mdx b/content/docs/(overview)/devices.mdx index 5a52a370..d71fdc35 100644 --- a/content/docs/(overview)/devices.mdx +++ b/content/docs/(overview)/devices.mdx @@ -49,7 +49,7 @@ The hydration **empties the copy first** if the instance's event log now ends be The app can then ask the person whether to build the copy again, which empties it, or keep it as it is. The CLI's `marfa device hydrate --in-place-only` does the same. -**Writes queued against one instance aren't sent to another.** When a hydration builds the copy from another instance than the one it was built from, each write still waiting is `blocked` with the reason `instance_changed`, because the instance now at the address may never have held the items the write was made against. A release sends it to that instance; a withdrawal takes it out of the queue. +**Writes queued against one instance aren't sent to another.** When a hydration builds the copy from an instance other than the one it was built from, each write still waiting is `blocked` with the reason `instance_changed`, because the instance now at the address may never have held the items the write was made against. A release sends it to that instance; a withdrawal takes it out of the queue. 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. @@ -95,7 +95,7 @@ A `blocked` write carries one of seven reasons. Six hold it until something chan | `ancestor_unavailable` | The instance no longer holds the version the write was based on. | A withdrawal, or a release once the credential has come to read that version. Sent again before then, the write is refused the same way. | | `conflict_unresolved` | A newer change collided with the write, and the instance didn't settle the two. | A withdrawal. Sent again, the write is refused the same way. | | `failing_alone` | The write keeps failing as if the server were down, while the writes behind it go through. | A release or a withdrawal. | -| `instance_changed` | The write was queued while the copy came from another instance than the one it's now built from. | A release, which sends it to the instance now at the address, or a withdrawal. | +| `instance_changed` | The write was queued while the copy came from an instance other than the one it's now built from. | A release, which sends it to the instance now at the address, or a withdrawal. | A **release** sends the write again in the next drain, under a fresh idempotency key. A **withdrawal** takes the write out of the queue and puts the copy back to what the instance holds. Withdrawing a create, or a write blocked `failing_alone`, also refuses the writes that wait on it. The Swift SDK's [queue](/swift-sdk#send-writes-and-read-verdicts) and the CLI's `marfa device release` and `marfa device withdraw` do both. From 476b1eacd3b9515076d93e558a3ca1f5cd5ebefb Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Fri, 30 Oct 2026 22:57:30 +0000 Subject: [PATCH 3/5] docs(devices): say a server refusal still ends an in-place-only fill --- content/docs/(overview)/devices.mdx | 2 ++ content/docs/run/troubleshooting.mdx | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/content/docs/(overview)/devices.mdx b/content/docs/(overview)/devices.mdx index d71fdc35..ad73913c 100644 --- a/content/docs/(overview)/devices.mdx +++ b/content/docs/(overview)/devices.mdx @@ -49,6 +49,8 @@ The hydration **empties the copy first** if the instance's event log now ends be The app can then ask the person whether to build the copy again, which empties it, or keep it as it is. The CLI's `marfa device hydrate --in-place-only` does the same. +A fill only in place guards against the device emptying the copy, not against the instance refusing the credential. **If the instance refuses the hydration because the credential has ended or its reach changed, the stale copy still ends**, as it does for any hydration, and the queued writes are kept. That refusal can also carry `reach_changed`, so before the app offers to keep the copy, it checks the copy's hydration state: a refused fill leaves the copy `stale`, and an ended copy is `expired`, which only a hydration that doesn't ask for a fill in place can build again. + **Writes queued against one instance aren't sent to another.** When a hydration builds the copy from an instance other than the one it was built from, each write still waiting is `blocked` with the reason `instance_changed`, because the instance now at the address may never have held the items the write was made against. A release sends it to that instance; a withdrawal takes it out of the queue. 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. diff --git a/content/docs/run/troubleshooting.mdx b/content/docs/run/troubleshooting.mdx index d8411b6c..14d966d5 100644 --- a/content/docs/run/troubleshooting.mdx +++ b/content/docs/run/troubleshooting.mdx @@ -279,7 +279,9 @@ An app built on the Swift SDK that holds a `changes()` stream refills a stale co After `instance_changed`, the hydrate builds the copy from the instance now at the address and holds each write still waiting as `blocked` with the reason `instance_changed`, so none reaches that instance by itself. Check `marfa device queue`, then send the ones you want there with `marfa device release --reason instance_changed`, or take one out with `marfa device withdraw `. -`marfa device hydrate --in-place-only` gives the same error, with the reason `instance_changed`, `reach_changed`, `slice_changed` or `head_past_log`, where the copy can't be filled again in place. It changes nothing. Run `marfa device hydrate` without the option to build the copy again. +`marfa device hydrate --in-place-only` gives the same error, with the reason `instance_changed`, `reach_changed`, `slice_changed` or `head_past_log`, where the copy can't be filled again in place. It changes nothing, and `marfa device status` still reports the copy's hydration as `stale`. Run `marfa device hydrate` without the option to build the copy again. + +If the instance itself refuses the credential or its reach during `marfa device hydrate --in-place-only`, the stale copy ends as it does for any hydration, and writes waiting in the queue are kept. `marfa device status` then reports the copy's hydration as `expired`, and only `marfa device hydrate` without the option can build it again. For a folder, run `marfa folders push` again instead: it builds the copy again itself. [Sync a folder](/run/sync-a-folder#replace-a-folders-key) covers a key you replaced. From ee00602ede6fe68e41d49e06c1aa3a988e56c7c2 Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Fri, 30 Oct 2026 23:39:56 +0000 Subject: [PATCH 4/5] docs(devices): say a refused fill leaves only a stale copy stale --- content/docs/(overview)/devices.mdx | 2 +- content/docs/run/troubleshooting.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/content/docs/(overview)/devices.mdx b/content/docs/(overview)/devices.mdx index ad73913c..ac0d1d27 100644 --- a/content/docs/(overview)/devices.mdx +++ b/content/docs/(overview)/devices.mdx @@ -49,7 +49,7 @@ The hydration **empties the copy first** if the instance's event log now ends be The app can then ask the person whether to build the copy again, which empties it, or keep it as it is. The CLI's `marfa device hydrate --in-place-only` does the same. -A fill only in place guards against the device emptying the copy, not against the instance refusing the credential. **If the instance refuses the hydration because the credential has ended or its reach changed, the stale copy still ends**, as it does for any hydration, and the queued writes are kept. That refusal can also carry `reach_changed`, so before the app offers to keep the copy, it checks the copy's hydration state: a refused fill leaves the copy `stale`, and an ended copy is `expired`, which only a hydration that doesn't ask for a fill in place can build again. +A fill only in place guards against the device emptying the copy, not against the instance refusing the credential. **If the instance refuses the hydration because the credential has ended or its reach changed, the stale copy still ends**, as it does for any hydration, and the queued writes are kept. That refusal can also carry `reach_changed`, so before the app offers to keep the copy, it checks the copy's hydration state: a refused fill leaves a stale copy `stale`, and an ended copy is `expired`, which only a hydration that doesn't ask for a fill in place can build again. **Writes queued against one instance aren't sent to another.** When a hydration builds the copy from an instance other than the one it was built from, each write still waiting is `blocked` with the reason `instance_changed`, because the instance now at the address may never have held the items the write was made against. A release sends it to that instance; a withdrawal takes it out of the queue. diff --git a/content/docs/run/troubleshooting.mdx b/content/docs/run/troubleshooting.mdx index 14d966d5..55cf9698 100644 --- a/content/docs/run/troubleshooting.mdx +++ b/content/docs/run/troubleshooting.mdx @@ -279,7 +279,7 @@ An app built on the Swift SDK that holds a `changes()` stream refills a stale co After `instance_changed`, the hydrate builds the copy from the instance now at the address and holds each write still waiting as `blocked` with the reason `instance_changed`, so none reaches that instance by itself. Check `marfa device queue`, then send the ones you want there with `marfa device release --reason instance_changed`, or take one out with `marfa device withdraw `. -`marfa device hydrate --in-place-only` gives the same error, with the reason `instance_changed`, `reach_changed`, `slice_changed` or `head_past_log`, where the copy can't be filled again in place. It changes nothing, and `marfa device status` still reports the copy's hydration as `stale`. Run `marfa device hydrate` without the option to build the copy again. +`marfa device hydrate --in-place-only` gives the same error, with the reason `instance_changed`, `reach_changed`, `slice_changed` or `head_past_log`, where the copy can't be filled again in place. It changes nothing: a copy `marfa device status` reported as `stale` is still `stale`. Run `marfa device hydrate` without the option to build the copy again. If the instance itself refuses the credential or its reach during `marfa device hydrate --in-place-only`, the stale copy ends as it does for any hydration, and writes waiting in the queue are kept. `marfa device status` then reports the copy's hydration as `expired`, and only `marfa device hydrate` without the option can build it again. From 2008a8ee15442a67fb7a279dd2c5e739ef03c5da Mon Sep 17 00:00:00 2001 From: August Cayzer Date: Sat, 31 Oct 2026 13:36:58 +0000 Subject: [PATCH 5/5] docs(devices): match the in-place and held-write wording to the v0.0.49 contract --- content/docs/(overview)/devices.mdx | 2 +- content/docs/run/troubleshooting.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/content/docs/(overview)/devices.mdx b/content/docs/(overview)/devices.mdx index ac0d1d27..1b5c6111 100644 --- a/content/docs/(overview)/devices.mdx +++ b/content/docs/(overview)/devices.mdx @@ -97,7 +97,7 @@ A `blocked` write carries one of seven reasons. Six hold it until something chan | `ancestor_unavailable` | The instance no longer holds the version the write was based on. | A withdrawal, or a release once the credential has come to read that version. Sent again before then, the write is refused the same way. | | `conflict_unresolved` | A newer change collided with the write, and the instance didn't settle the two. | A withdrawal. Sent again, the write is refused the same way. | | `failing_alone` | The write keeps failing as if the server were down, while the writes behind it go through. | A release or a withdrawal. | -| `instance_changed` | The write was queued while the copy came from an instance other than the one it's now built from. | A release, which sends it to the instance now at the address, or a withdrawal. | +| `instance_changed` | The write was made against an instance other than the one the copy is now built from. | A release, which sends it to the instance now at the address, or a withdrawal. | A **release** sends the write again in the next drain, under a fresh idempotency key. A **withdrawal** takes the write out of the queue and puts the copy back to what the instance holds. Withdrawing a create, or a write blocked `failing_alone`, also refuses the writes that wait on it. The Swift SDK's [queue](/swift-sdk#send-writes-and-read-verdicts) and the CLI's `marfa device release` and `marfa device withdraw` do both. diff --git a/content/docs/run/troubleshooting.mdx b/content/docs/run/troubleshooting.mdx index 55cf9698..1af96a66 100644 --- a/content/docs/run/troubleshooting.mdx +++ b/content/docs/run/troubleshooting.mdx @@ -288,7 +288,7 @@ For a folder, run `marfa folders push` again instead: it builds the copy again i -`marfa device hydrate --in-place-only` fills only a copy whose cursor the instance's event log let go, and this copy is whole, was never built, or has to be built again. Nothing was changed. Run `marfa device catch-up` to bring a whole copy up to date, or `marfa device hydrate` without the option to build it again. +`marfa device hydrate --in-place-only` fills only a copy whose cursor the instance's event log let go, and this copy is whole or was never built. Nothing was changed. Run `marfa device catch-up` to bring a whole copy up to date, or `marfa device hydrate` without the option to build it again.