Your data on several devices¶
Built: sharing through a folder your sync carries (Nextcloud, Dropbox, Syncthing), or that Sioul keeps in step with a Nextcloud itself (below), set up in Settings ▸ Between your devices, part by part on each device; a new device joins through its Nextcloud's own sign-in (below, "Joining through your Nextcloud's sign-in"). Why plain files and a log, rather than a database server, git or whole-folder archives: below. Code: crates/sioul-sync/src/share.rs (the log, the parts), blobs.rs (notes and papers sealed apart), history.rs (earlier versions), remote.rs (the folder read on its server too, and this device's files sent there), loginflow.rs (a Nextcloud's sign-in, for a device joining), trigger.rs (an exchange as soon as a shared file is saved), crates/sioul-app/src/share.rs, join.rs, qml/SharePanel.qml and qml/JoinPanel.qml (the window).
What travels how¶
- By their servers: mail (IMAP), contacts, events and tasks (CardDAV, CalDAV, Google), with what Sioul writes in each task (kinds, projects, links, steps, waits: RFC 9253).
- By their own folder, or by the log: the notes folder: projects and their routes (
sioul-projects.toml, orsioul-cases.tomlunder its first name), budgets and bank accounts (sioul-budgets.toml), the bank's movements (sioul-bank.toml), contracts (sioul-contracts.toml), papers (sioul-papers.toml,papers/), notes, their pictures, PDFs and memos. A synced folder carries them itself; a folder no sync carries (a phone's: eDrive syncs only its own folders) sends them through the log, sealed, when its parts are switched on (below). Never both on one device: a file carried twice, by the sync and by the log, would undo changes. The Settings page says when the notes folder seems outside every synced folder (Nextcloud's own list of folders,~/Nextcloud,~/Dropbox,~/Sync…) and notes are not shared through the log. - By the log: what only this computer keeps, and the notes folder when asked, in parts:
| Part | Files | Shared as |
|---|---|---|
settings and accounts (settings) |
config.toml, links.toml, porch.toml, money.toml (mail said to be no payment), do-not-disturb.toml (do-not-disturb's switch, do-not-disturb.md), pgp/cards/ (your security keys as a computer of yours read them, their certificates with them, public parts only: client.md, "Your keys on a security key"; a store 0.0.6 and older do not know: they keep its changes waiting) |
one entry per setting, tie, mark, word; accounts one entry each (by id); each account's Porch mark whole; the switch a table per device, whole, written by that device alone; in format 2 (below, "The format of what travels") pinned sites, routines and task kinds one each (by id), days off one each, each word added to a list or taken from it one entry (by the word), the sites' order one entry (site_order); in format 1 each of these lists whole; each security key's file whole, written by a computer that read the key |
senders (senders) |
known-, blocked-, safe-, neutral-, restricted-senders.txt; dnd-people.toml (who may reach you during do-not-disturb); shield/; pgp/others/ (others' keys, each with the addresses it serves beside it, <fingerprint>.toml, format 2, which an older Sioul carries without reading: client.md, "What others' keys serve") |
one entry per line (an address or a pattern, tel: a number or a prefix, contact: a card's UID, category: a category); one per person (by id); one per file |
calls (calls) |
state/calls/log/ (each phone's log of the calls it screened) and state/calls/seen/ (each device's Seen) |
one entry per line, each file written by its device alone (below, "The calls' logs") |
spam filter (spam) |
spam/table.bin (the table a training makes on a computer, spam-filter.md); state/spam/labels/, state/spam/moved/ and state/spam/flagged/ (each device's label log, and its logs of what the filter moved and flagged) |
the table: one record, the file sealed apart (below, "The spam filter's table and label logs"); the logs: one entry per line, each file written by its device alone |
health (health) |
health.toml, health-days.toml (each day's own meals, naps and nights), health-state.toml, health-doses.toml (each dose that fell due, and the answers captured), health-reminders.toml (what each device did to remind each dose: health.md, "What became of a reminder"; a file 0.0.5 and older do not know: they keep its changes waiting, forever, so every device sharing the doses is updated at once) |
one entry per setting, mark, dose, field of a day's block; prescriptions and medicines one each (by id); a dose's record field by field, each device's opening apart, each device's answer whole; each device's word on a dose's reminders whole |
time (time) |
time/<month>.toml, time/running.toml, today.toml, stopped.toml (where you stopped), quiet.toml (the hours' overrides and the two pauses, pauses.md), reviews/<month>.toml (how each day went, reviews.md) |
one entry per session (format 2: by its id, written in at the switch for an older one; format 1: by its start, task and project), setting, word, lighter day (format 2; format 1: their list whole); the session running now whole; a day's review at the end of work, and before sleep, each whole |
drafts and invoices (drafts) |
drafts/ (each draft, and the copy of the message it answers or forwards as it came: encrypted when it came so, client.md, "Encrypted messages you answer"), invoices/ |
one entry per file |
projects and money (projects) |
from the notes folder: sioul-projects.toml (its record still notes/sioul-cases.toml, as every version reads it), sioul-budgets.toml, sioul-bank.toml, sioul-contracts.toml |
projects, budgets, presets, reserves, bank accounts and contracts one each (by id); a choice for one movement by its account and movement; lines, covers, mail rules, splits and presets one each, by their id (format 2, each given one at the switch at the latest; format 1: as themselves, presets by id); the bank's accounts by id, its movements by account and the bank's own id |
lists kept here (lists) |
calendars/local/, contacts/local/, their state in dav/local/ |
one entry per file |
notes (notes) |
the notes folder, less what the other parts carry | one record per file, the file sealed apart (below) |
papers (papers) |
sioul-papers.toml, papers/ in the notes folder |
papers one each (by id); one record per file, sealed apart (a paper's file kept outside papers/ stays on its device) |
-
Never: the spam filter's corpus, outside material and language model (
spam/corpus/,spam/external/,spam/language.bin: they stay on the computer that made them; only its table and the label logs travel), which parts each device shares (its own choice), where things are on each device (the notes folder, a sender list's own path, each account's mail folder, how far back mail goes), how text reads on this screen (reading), how pages are laid out (tasks-view.toml), this device's browser notices, your own PGP keys (secret keys never leave the device they were made on: copy them by hand; a security key's certificate travels, public parts only, inpgp/cards/), the security keys this device stopped using (pgp/stopped.toml: what another computer shares of them is passed over here) and those it shared once already ($XDG_STATE_HOME/sioul/security-keys.toml, so that a file another computer took out is not written again), whether others' keys kept before were given what they serve ($XDG_STATE_HOME/sioul/pgp-others.toml, done once), the verdicts of the look at the copies kept with drafts ($XDG_STATE_HOME/sioul/draftcopies.json: each file's path, its device's and inode's numbers, its size and its time, never a word of it: client.md, "Encrypted messages you answer"), sync's state, caches, the keyring. Passwords and tokens stay in each device's keyring: an account arriving from another device asks for its password once. -
What a calendar takes (21 October 2026): events, tasks or both, kept with the calendar, in its state (
$XDG_STATE_HOME/sioul/dav/<account>/calendars/<id>.toml,components; this device's own indav/local/, which the part "lists" carries). A server's is what the server says at each sync (supported-calendar-component-set, RFC 4791 §5.2.3; unsaid, both; Google's calendars, events). This device's own get theirs when made (a task list, tasks; the calendar for time blocks, events), written before its folder shows, so that a look meanwhile never settles it as both (until 22 October 2026 a sync or the window looking at that moment could); one made before without them is settled once from what it holds (events alone, tasks alone, else both:vdir::settle_kinds). What a calendar's folder holds counts too, at each read (read again when its files change): a task that came into a calendar said to take events alone (another device's files arriving one by one, an older Sioul, an import) shows among the task lists, never hidden, and what is saved is not changed by it (vdir::kinds_held; review of 22 October 2026, F3). Every view goes by it: the agenda reads the calendars that take events (a task shows there by its time block, marked as a task); the task views the lists that take tasks; a new event is offered the calendars that take events, a task the lists that take tasks (the others greyed). One that takes both is in both, and its name says so everywhere it is offered ("Personnel · murena.io · events and tasks").
Parts¶
Each device chooses what it shares, part by part (Settings ▸ Your folder and sharing ▸ What travels from this device): [parts] in its $XDG_STATE_HOME/sioul/share/here.toml, never shared. A device that never chose shares what was shared before parts had switches: everything but notes and papers (the spam filter's table, which came later, included; never the phone's messages nor the texts, which came later still: each device shares them once you turn them on there), and projects and money as share_projects said (a setting every device followed then; the first time sharing is on with this version, what it said becomes this device's own choice, and the old key is still read until then). A part switched off takes nothing out elsewhere and sends nothing; the others' changes for it are not kept waiting (switched on again, it joins as a new device joins, below: "Stores that leave, arrive or move", and reads them then). Notes and papers are refused on a device whose notes folder a sync app already carries, with the sentence why: the two carriers would undo each other's changes. Known carriers: on a computer, a folder inside one named like a sync's (~/Nextcloud, ~/Dropbox, ~/Sync…), Nextcloud's own list of folders, Syncthing's (its config.xml) and Dropbox's wherever it is (its info.json); on a phone, where Sioul cannot read the sync apps' settings, as you say there: before Notes or Papers is switched on, the panel asks whether a sync app on the phone carries the notes folder, and a refusal shows a button to say none does (Here::notes_carried, never shared); the answer is for that notes folder (Here::notes_carried_for): the notes moved elsewhere, it is asked again, the guess holding meanwhile (until 10 October 2026 it stayed, and notes moved into a folder eDrive carries travelled twice: share::tests::the_answer_on_the_notes_folder_is_for_that_folder); until you said, guessed: a notes folder in the same top folder of its storage as the sharing folder (Documents for eDrive), whatever name the storage goes by (/sdcard, /storage/emulated/0). Other sync apps (Google Drive, Insync, rclone, MEGA) are not recognised: leave Notes off where they carry the notes folder. Before Notes or Papers is switched on, the panel says how many files and how many bytes would travel. On Android, notes and papers wait while Sioul lacks "All files access" (without it a folder lists only what Sioul made, and the rest would seem gone): said. Each part shows what it carries and when it last sent and received a change.
Notes and papers in the vault¶
- One file at a time: each file of the notes folder (or of
papers/) is a record,files/notes/<path>→ its content's hash (SHA-256), its size, its time, and the content it replaced where it changed (b, its base). The content itself goes apart, once: compressed (gzip), sealed in pieces of 1 MiB (XChaCha20-Poly1305, each piece bound to its file, its place and whether it is the last, so a piece swapped, cut or moved does not open; nothing may follow the last), under<folder>/blobs/<name>, the name a keyed hash of the content (HMAC-SHA-256), which tells the server nothing, not even that two files are the same. The keys for sealing and for naming are made from the sharing key (HKDF-SHA-256, one for each use, for this format:sioul-blob 1); the records' key itself seals no file. Written once, under a hidden name of its own then renamed. A change sends one file; a file in two places, or back as it was, is sealed once. A name may hold#("Facture #123.pdf"). - Only what changed: a file whose size and time did not change since it was last read, read more than a moment after it changed, is not read again; one whose time changed is read through for its hash, and sent only when its content changed. A file changed less than two seconds ago (maybe still being written) is read at the next exchange. A note or a paper of 1 MiB or more waits until it has not changed for ten minutes, or an hour after it was first found changed, whichever comes first (
share::LARGE_SETTLES; when each began to change is kept infiles.json,Sealed::changing): a recording or a scan saved again within minutes goes once, its last save, and one that never stops changing, a new one too, goes at the hour. On 14 October 2026 four earlier takes of the owner's recordings, 15.7 MB, were sealed 13, 16, 25 and 1 minute before the next save, and each stayed in the folder as long as replaced contents stay: ten minutes would have kept only the last of them (3.5 MB) off it. Until the review of 15 October 2026 the hour counted from when the file was last read, so that one saved again after a quiet hour went at once. Meanwhile the others' changes to it wait, and nothing of it is taken out, as for a file being written; smaller notes and the spam filter's table go at once (share::storage_tests::a_large_note_is_sealed_once_it_has_settled). Files are read and written a piece at a time, never held whole in memory; a sealed file opened inflates 64 KiB at a time and stops past 64 MB, whatever it says. Over 64 MB a file stays here, said (share-too-big). A name that is not text stays here, said (share-not-text). Left out: what sync apps, editors and systems make (hidden files,~and.tmpfiles, conflicted copies of Nextcloud, Dropbox, Syncthing, ownCloud, Seafile, Office's~$locks), links, tools' folders (node_modules,target), a folder shared through (it holds a seal), and what the other parts carry (the manifests,papers/in the notes, whatever their case). What Sioul left half written in a crash (its hidden.sioul.tmpfiles) is cleaned an hour later. - Coming in: nothing is written without room for it, for the copy kept of the file it replaces and some left (a quarter of a GB, or a hundredth of the disk:
disk::kept_free): else it waits, said (share-no-room). The content is opened beside its file (a hidden name), checked against its hash; just before it takes the file's place, the file is looked at again: changed since the exchange looked (an editor saving), nothing is written over it, and the next exchange sends that change first, both kept. Then the file as it was is kept (Earlier versions), the content takes its place, dated as where it was made (never after this computer's clock), and what the next look compares with is what was written: a change made after it is a change made here, sent. A file taken out takes its folders with it when they are left empty. Never written: through a link in the notes folder (a link to elsewhere would carry the write out of it), over a link, into the sharing folder (it would travel in plain) or Sioul's own state (share-refused); nor a name this device's storage would take for another file's, by case or by how an accent is written (share-name-clash: a phone's, a memory card's, Windows', a Mac's); nor, on Windows, a name Windows keeps for itself (aux.md). Not there yet (a slow sync app, an empty placeholder of a file kept on demand): it waits, said after a day (share-missing); where the folder is found on its server, that sealed file is fetched from there by its name, and the note written at once (below, "Fetched from the server too": "Sealed files fetched when waited for"). Damaged (it does not open whole, or opens to another content): said (share-damaged), the file here stays as it is, that copy is not opened again until another copy comes (another size, time, or place on the disk), and a later version comes as ever. A failure here (a full disk, a folder that cannot be made) is tried again after a minute, then ten, then an hour, then six: not at every exchange. - Changed on two devices: a file here that is neither what the incoming change replaced nor a content the others' records named for it (their versions and bases, kept while a change waits) changed here too. It is copied beside first, "lease (conflict 2026-10-05 16.05).md", the same words in every language, dated by when its version was made, so that every device holding that version names it alike and one copy travels (the notes editor names the version it saves over a changed note the same way); then the later version takes the name. Said for a day (
share-conflict). A file taken out on one device and changed on another: what changed is kept the same way (share-conflict-gone). A device joining with files of its own that differ is the same case: its versions are kept beside. - An older copy put back by hand: a file whose time went back past the version last seen here (a backup restored with its old dates) is not a change: that version comes back from the folder, the older copy kept beside, said (
share-older-copy). To send an older version on purpose: Put back (Earlier versions), or give the file today's date. - Taken out: a file deleted here is taken out elsewhere once it has been gone ten minutes (a file being replaced, a disk away a moment), a record taken out with its base; meanwhile another device's change to it still comes. A device that takes it out keeps it among its earlier versions as a copy, moved there rather than copied (
history::keep_moved: no disk spent), never by reference: its content may leave the folder as soon as every device read that it was taken out (below, "Old contents go"). Until 17 October 2026 it was kept by reference when its content was in the folder. Where it cannot be moved (on Android the notes and Sioul's own storage are apart; a file with other names, hard links, is copied so that its other names keep their own permissions), it is copied, 64 MiB at most an exchange (share::TAKE_OUT_COPIES): the rest wait for the next ones, said in the outcome ("copies"). Measured on a computer (17 October 2026, release build, ext4): 7,000 take-outs of 500 bytes kept as copies took 1.0 s, moved 0.36 s (history::tests::what_a_bulk_s_copies_cost, run with--ignored); a phone's flash is slower. A dose's exchange never waits for them: a hurried exchange leaves the rest for the next (copies_of_take_outs_are_bounded_per_exchange). - Removed notes: many at once (more than ten, or three and more past a fifth of the folder) from a folder that lists as it should (deleted in bulk, a folder moved within the notes) are held two weeks from when they went (
share::GONE_GRACE,blobs::UNUSED_DAYS; when each went is kept,Sealed::gone): nothing of them is taken out elsewhere meanwhile, nor written into them; then they leave your other devices by themselves, nothing pressed, unless they came back. Meanwhile the Porch says it once, calmly: how many went, from which folders (the deepest folder that holds four in five of them; else the few, three at most, that hold most; else the folder above them all:share::bulk_folders), when, and when they leave; with Put them back (share::put_back_withheld: the files held as the Porch counts them, never a note deleted on purpose beside a bulk's remainder (Sealed::bulk, as Take them out now; review of 18 October 2026, I-1); each written back where it was from its content sealed in the folder, dated as it was last seen here, or, held by a Sioul up to 17 October 2026, which kept no such date, as its last record was made; never over a file there, one written meanwhile included (a hard link takes only a free name), never through a link nor into the sharing folder, as nothing the sharing writes is, never without room (share-no-room); on the worker's thread, never the window's, a quarter of a second's worth at a time, each file opened under a hidden name in the deepest of its folders that is there, outside the exchanges' lock, taken only to make its own folders, date it and place it after a last look under that hold, a fifth of a second of placing at a time (share::in_short_holds,SHORT_HOLD), a hold ending after the file it places when an exchange of this process comes to wait (share::waiting_here), then handed to that exchange before it is taken again (share::hand_off: it waits until that one has taken it, or until fewer wait (one that gave up, or lost the lock to another process: review of part 4, D-1a), a second at most; the copies beside drafts the same, their hold ending after its copy for a waiter, let go at once when none waits (D-1b); with none waiting, a moment, which a waiter in another process, the command line, gets alone, as a file's lock keeps no queue: it may wait a whole hold, a fifth of a second; D-1: on CI's macOS job of 20 October 2026 a dose's exchange slow to wake lost the hand twice in a row, 0.41 s; a waiter taking the lock 50 ms after each release: 0.48 s with the moment alone, 0.06 s now; one waking at once, 0.01 s,put_back_never_keeps_a_slow_waking_dose_waiting,put_back_on_slow_storage_never_keeps_a_dose_waiting) (a folder made for it holding hidden files alone would list empty to an exchange, which takes it for emptied and lets its notes leave the hold, for good after a stop meanwhile: review of 18 October 2026, L-1;a_put_back_makes_no_folder_an_exchange_takes_for_emptied,a_put_back_cut_short_leaves_its_notes_held), so that a dose's exchange waits at most that fifth of a second, the one file placed as it passes and the lock's own taking, even behind a note or paper of tens of megabytes, and on storage that takes tens of milliseconds a file (until 18 October 2026, opening a 40 MB note within the lock held it 0.62 s here; until 20 October 2026 a whole batch, up to 200 files, was placed in one hold, and a dose's exchange waited 1.1 s behind 1,500 notes on CI's Windows job) (on this computer, 20 October 2026: 1,500 notes put back, 13 ms at most, 230 exchanges,put_back_never_keeps_a_dose_waiting; 120 notes each placed 10 ms slower, 0.19 s at most,put_back_on_slow_storage_never_keeps_a_dose_waiting, through a delay that only the tests build,share::SLOW_UNDER_LOCK; before 18 October 2026 the put back took the lock again at once, and a dose's exchange waited up to 14 s); each dated as it was only under the lock, just before it is placed, through a handle that cannot write it (share::set_time: on Windows a handle on its attributes alone, since closing one that could write the file has the antivirus read it whole before the close returns, which kept a dose's exchange waiting 0.27 to 0.38 s behind a 24 MB note on CI's Windows job of 19 October 2026; the same quarter of a second is the bound there, the job's own measure; elsewhere a read handle, the owner setting its times): an exchange's sweep of leftovers takes a hidden file dated more than an hour ago, and none runs while the lock is held (until 18 October 2026 a bulk of notes last changed days ago lost most of its files to the window's exchanges and was said done: 600 notes beside them now all come back,a_put_back_beside_the_exchanges_brings_every_note_back); the next exchange finds them back, and nothing leaves; fewer than were held is said ("11 of 12 came back. One did not: another file or a link is at its place, or its content is not on this device."), never as done, and the Porch keeps the bulk with Put them back; nothing held any more, nothing is said) and Take them out now (share::confirm_withheld: the files held, those alone; a folder that lists empty beside them, a disk not mounted, stays unknown until its own line says so: review of 17 October 2026, M1). A file held so stays held until its own two weeks are over, whatever leaves beside it (Sealed::bulk: until 17 October 2026 a bulk's last few went at once once the rest had left, no longer "many"). Shown a few seconds, it is not said again when the Porch is left (a view flag per bulk,porch-withheld-said:<folder>:<when>: a later bulk is said anew); Settings ▸ Your folder and sharing says it as long as it lasts, with the same two buttons. What the last exchange held is carried in its outcome (share::Outcome::withheld,Memory::withheld), so the window never reads the sharing's memory for it. Until 17 October 2026 they waited for "Take them out everywhere", however long that took (share::gc_tests::notes_gone_at_once_leave_by_themselves_after_two_weeks,notes_held_come_back_when_put_back,notes_held_are_never_put_back_through_a_link,take_them_out_now_takes_out_the_bulk_alone_never_a_folder_listing_empty,put_back_keeps_its_care_and_dates_an_older_bulk_by_its_records,a_bulk_s_remainder_leaves_in_its_own_time,a_bulk_s_folders_are_said_shortly). - A folder missing or unreadable whole is not a deletion: a folder that lists empty while it held files at the last look, one that cannot be read whole (one name that errs is enough), or the notes folder gone (a disk not mounted, an access withdrawn), or a folder or a note replaced by a link (moved out of the notes and linked back in its place: links are never followed; a note is said as the note it was, "is a link here now"): its files are unknown, never gone, and nothing of them leaves, two weeks on or two months on; nothing is written into them; said (
share-vanished), until you say "Take them out everywhere" on its own line (share::confirm_gone: that folder and what is below it, never a folder beside it); or switch the part off and on to fill the folder again from the others (a_notes_folder_missing_whole_never_leaves,a_folder_replaced_by_a_link_never_leaves). - Old contents go: a sealed file no record points to any more is removed by the device that wrote it two weeks after the last record that did (
blobs::UNUSED_DAYS; the spam filter's tables sooner: below). A device needs a replaced content only between reading the record that names it and the one that replaces it, and a device back after a long time reads both in one exchange. Until 15 October 2026 it was 90 days: on that day the owner's folder held 349 MB, a third of the account's space, of which some 25 to 45 MB were replaced contents and 47 MB a phone's past full rounds (below, "What the folder holds over weeks"). Never one another device's record named in the two weeks since this device first read that record (named_at: never from this device's own records, nor from a record read again: until 15 October 2026 each store rebuilt or added by a new version read this device's kept rounds again and dated every replaced content they named anew, so that none aged while new versions kept coming:share::storage_tests::a_rebuild_leaves_the_times_others_named_as_they_were), nor one whose file was made fresh lately: a device using again a content sealed more than a week before makes its file fresh (blobs::FRESH_DAYS), and its sync app carries the new time, eDrive too. A content replaced in an exchange counts as used in it: a device back after weeks away that replaces a note at once keeps the old content two weeks from then, not from its last use noted before it left (a_content_replaced_on_coming_back_stays_two_weeks_from_then). Every ten minutes, each device looks for the sealed files of its own records and seals again any gone or not of the size it sealed, while its file still holds that content. The content of a note or paper taken out goes sooner: as soon as every device that counts read past the round that took it out (strictly: a reader enters the next round once it read that one to its end;share::removed_due,Sealed::removed, the writer and round of that record), its last use counts as two weeks ago, and the sweep takes it with its care all the same: never one used again or named lately, never one made fresh, never one a record replaced within thirteen days (this device's own changes and the others' read here,Sealed::replaced: the 12 days a version is kept by reference, and a day): another note may hold the same bytes, and a device keep its earlier version by reference (review of 17 October 2026, M2;a_content_replaced_lately_never_leaves_early_and_its_version_puts_back). Only once every device that counts says it keeps a file taken out as a copy (keeping2,blobs::KEEPING_MOVED; below, "Devices"): a Sioul up to 17 October 2026 keeps it by reference, 12 days, and then the content stays two weeks from its last use, 90 days while an older Sioul still counts, as before (share::gc_tests::a_note_and_a_paper_taken_out_free_their_contents_once_every_device_read_so,a_content_taken_out_stays_its_time_while_an_earlier_sioul_counts). A note edited many times keeps its earlier versions by reference 12 days on each device that received them, and their contents two weeks in the folder, after those references (a_note_edited_many_times_keeps_its_versions_then_their_contents_go). With eDrive, which never deletes a file deleted elsewhere, the phone keeps every sealed file its sync app brought; on the server, each device sending its own files takes the ones it swept out there too (below, "Sent to the server too"). - Two weeks once every device keeps them so: each device's sealed entry says how it keeps sealed files (
keeping, below, "Devices": 1 from 15 October 2026, 2 from 17 October 2026, which keeps a file taken out as a copy). A device keeps its own two weeks only once every other device that counts says 1 at least (share::unused_days,blobs::KEEPING_TWO_WEEKS); while one runs an older Sioul (0.0.5 and before, whose entry says nothing of it), 90 days, as that Sioul counts on: it makes a content it uses again fresh only past 30 days, so a phone of that kind under eDrive, which keeps every sealed file it brought, could put back an earlier version whose content left the server, and name it in a record that no device could follow. A device known by its records alone, or whose entry does not read, counts as an older one; a hostile folder can only make contents stay longer (replaced_contents_stay_ninety_days_while_an_older_sioul_shares). - A device that no longer counts (it left, you forgot it, another device's entry replaces it: a phone dead for good) never sweeps its own again. The contents its records named that no record names any more are swept by each device that counts, as that device sweeps its own (
share::sweep_theirs): each is noted when it stops being current there, with the device whose record named it (Sealed::theirs; forgotten once its writer swept it), and goes the same two weeks (or 90 days, above) after its last use there; never one a current record names, a change waiting, one the others named lately, one made fresh, one this device sealed itself, nor a medium a text names (share::texts_media: read from the texts' logs on a device that shares the texts; a device that does not sweeps nothing while a phone may have brought one; among computers only, no phone ever seen, there is none); and nothing at all while this device does not share a part whose files are sealed apart (the notes, the papers, the spam filter's table:share::FILE_PARTS): it passes that part's records over, and a current paper with the same bytes as a departed device's replaced note is the same sealed file (a_device_without_the_papers_sweeps_nothing_of_another_device_s; review of 17 October 2026, L1). A device's own sweep has that blind spot, as it always had: a content it replaced whose bytes a current paper of another device's holds goes two weeks on, on a device that does not share the papers, and comes back as soon as that paper's writer looks for its sealed files (every ten minutes, while its file holds that content); the devices holding the paper keep it. Each one swept goes from the server too, at a listing, only while it is still as old there (below, "Sent to the server too"). Until 16 October 2026 they stayed for good (share::rounds_tests::a_dead_device_s_unused_sealed_files_go_never_a_content_named,among_computers_a_dead_one_s_replaced_contents_go_when_forgotten). - A device away for a long time: it reads at once all it missed and writes the latest version of each file. Away more than two weeks, the versions replaced meanwhile are gone from the folder: it never needs them, but nobody can put them back. Its own files as they were before it caught up are kept among its earlier versions, as copies when their content left the folder too (
a_device_back_after_weeks_keeps_its_own_last_version). A round of another device it has not read is kept for it until it reads past it, as long as it counts: heard of within 180 days by a sealed fact (its entry, its records), and not forgotten, gone or replaced (below, "The log"). Forgotten and back within 180 days, it counts again at once, reads from the oldest round kept and finds every entry there, and every one taken out meanwhile (below, "The log"); a note it never changed here is no conflict, even when it changed twice meanwhile and only a full round says so: the version it last received from another device is an older one, never a change of its own (share::receive; until 16 October 2026 it made a conflicted copy of each:share::rounds_tests::a_forgotten_device_that_comes_back_finds_every_entry). - Its own memory: what an exchange remembers of notes and papers (their entries, their files as last read, the changes waiting), and of the logs of lines (calls, the phone's messages, texts), is kept in
share/files.json, apart fromshare/memory.json, which what the doses and claims ask reads alone. That file lost or unreadable, notes and papers join again as a new device's would: nothing goes out as new. It is written only when what it holds changes: an idle exchange writes nothing of it (share::tests::an_idle_exchange_writes_nothing_with_ten_thousand_texts, half an hour idle with ten thousand texts). A log's lines are kept once, as its entries, not again in its file's state (ten thousand texts, 5.5 MB, where 9.7 MB were): a log is read through at each exchange, or, in a process that keeps the memory between its exchanges (a phone's background service, below, "An exchange with nothing new"), only once it changed. The times that change on their own (when the sealed files were last looked for, the history last tidied) are kept inmemory.json; a sealed file still used is said so once a day, not at every exchange. Until 8 October 2026 that file was rewritten whole at every exchange (on the owner's computer, 25 MB every 12 to 50 seconds, with 9,833 texts). The window gives back to the system what an exchange held a moment (glibc'smalloc_trim).
The spam filter's table and label logs¶
The table a training makes on a computer (spam-filter.md), about two megabytes, replaced at each training that does no worse, read by every device, the phone's background step included. One store, files/spam/table.bin (share::SPAM_TABLE), in its own part, Spam filter, on by default.
- Sealed apart, as notes are (
Shape::Files): the record says its content's hash, size and time; the content goes once, as a sealed file inblobs/. Whole in a record (Shape::Whole), its two megabytes, some 3.6 MB sealed in a line, would go again in every round its computer starts, every device would read them again in each, eDrive would keep each round for good, and with Sioul keeping the folder itself the round being written goes up whole at each change: every minute, while a computer writes. - In every exchange, unlike notes and papers: a single file sealed apart is Sioul's own, quick to read, in its private folders (a phone needs no "All files access" for it), so a dose's alarm, a button and the phone's background step carry it too. A device without a table yet has none, not an unknown one: the first one is written.
- Two tables in the folder: the computer that sealed them takes its own older tables out of the folder at once, past the newest two (
share::KEPT_TABLES,prune_tables), not 90 days on: a training a week left a dozen there, two megabytes each. Never another device's file. Every device reads the newest record of a file only, an older Sioul too, so a table taken out is never asked for; an earlier version of the table kept by reference on another device can no longer be put back from the folder. Which tables are its own is in this computer'sshare/files.json, never shared: those sealed before 9 October 2026 go as other replaced contents do (above, "Old contents go"). A phone synced by eDrive still keeps each table eDrive brought it, since eDrive never deletes a file deleted elsewhere (share::tests::only_the_two_newest_spam_tables_stay_in_the_folder). - The one it replaces is kept beside, as
table.prev.bin, when this Sioul can use it (spam::table::keep_previous); a table this Sioul refuses (another version's tokenizer or header features, damaged) is written all the same, and the one before stays in use until a table it reads comes (Table::cached). The Earlier versions below keep it too, by reference. - Never the language model, the corpus nor the outside material beside it: no store names them (
share::tests::the_spam_table_travels_alone_in_every_exchange). - One computer trains, by hand: two computers training is not supported. Should both train, the later table wins everywhere, as for any file; a table trained on each before they exchange keeps both, the other as "table (conflict …).bin" beside it, which nothing reads: a safety, nothing more.
- The label logs travel with it, in the same part, as folders of lines (
share::SPAM_LABELS,share::SPAM_MOVED,Shape::Lines): each device writes its own file (<device>.jsonl, its name in the sharing), a line per act, never rewritten; every device reads them all, in every exchange, quick ones included. So a message said not spam on the phone is never flagged again on the computer, and the training there learns from it (share::tests::a_label_on_one_device_reaches_the_others).
The calls' logs¶
The calls your phones screened (android.md, porch.md, "Calls declined"), in their own part, Calls, on by default: a computer that others see can take your lists without taking who called.
- Two folders of lines (
share::CALLS_LOG,share::CALLS_SEEN,Shape::Lines): each phone's own log,state/calls/log/<device>.jsonl, a line per call, declined or let ring (never a blocked caller's); each device's own Seen,state/calls/seen/<device>.jsonl, a line per press. Each file has one writer; every device reads them all, in every exchange, quick ones included. A line at a time, not a month at a time: a month sent whole would go again at each call, or leave a sealed copy at each change in the folder, which eDrive never deletes. - A month: each device takes its own lines out after 31 days (
calls::trim_own, once a day); the sharing takes them out everywhere. Another device's lines 45 days old are taken out by any device (calls::trim_others): a phone gone for good never takes its own out; while it shares, it took them out long before. - Remembered beside: a store of lines names each entry by the line itself, so the calls' entries are kept in
files.jsonwith the notes', not inmemory.json, which every exchange writes and the doses' knowing reads (share::calls_store);files.jsonlost, they join again rather than going out again as new. - No earlier versions, no first copy: never kept in
history/nor copied intobefore-sharing-<day>/: a log has nothing to put back, and a call leaves every device after its month. - An older Sioul knows neither folder (
known_part): their records wait in its memory, written nowhere, taken out nowhere; updated, the stores join and the calls come in (share::tests::an_older_sioul_keeps_the_calls_waiting). - Tested (
cargo test --release -p sioul-sync share): a phone's call reaching the desk and the desk's Seen reaching the phone, sealed in the folder, remembered besidememory.json, no versions,files.jsonlost (a_phone_s_calls_reach_the_desk_and_seen_comes_back); several phones (several_phones_each_keep_their_own_log); an older Sioul; the part switched off (the_calls_part_switched_off_receives_nothing_and_keeps_nothing_waiting); a line taken out by its phone a month on, gone from the desk's copy (a_trimmed_line_leaves_every_device).
The phone's messages¶
The messages your phone's notifications brought, for the apps you chose on the phone (android.md, porch.md), in their own part, Messages from your phone (phone-messages), off until you turn it on, on each device: on the phone to send them, on a computer to show them there. A notification's words leave the phone this way only, sealed, and only for the apps you chose.
- Five folders of lines, as the calls' (
share::PHONE_MESSAGES_LOG,share::PHONE_MESSAGES_SEEN,share::PHONE_MESSAGES_LIVE,share::PHONE_MESSAGES_ASK,share::PHONE_MESSAGES_OUTCOME,Shape::Lines): each phone's own log,state/phone-messages/log/<device>.jsonl, a line per message, written again when the phone lets it through more than a minute earlier than first said, and read once by its id, at its earliest (sioul_core::phonemsgs::Line: when, the app, the kind, the conversation's key and a group's title, the notification it came in by the phone's own name for it (a hash; Android's key never leaves the phone), who as the app named them and their number's key, the row the phone judged them by, the words cut at 1,000 characters or none, "a picture", "a code", when the phone let it through); each device's own Seen,state/phone-messages/seen/<device>.jsonl, a line per press; each phone's own live lines,state/phone-messages/live/<device>.jsonl, a line per change of what a relayed notification offers a reply from a computer (sioul_core::replies::Live: the notification's name, its app, its conversation's key, a Reply, a Mark as read, when it went; no words, no names; android.md); each computer's replies to send,state/phone-messages/ask/<device>.jsonl(sioul_core::replies::Ask: a random key, the computer that wrote it (sealed in the line: one found in another device's file is passed over), the phone, the verb, the notification's name, the line answered, the words, when; each line padded to 256 bytes and sealed at rest with the replies' own key,textseal::ReplySeal, HKDF "sioul-replies 1"); each phone's word on them,state/phone-messages/outcome/<device>.jsonl(sioul_core::replies::Outcome: the key, the state, when, why; never the words). One writer per file; every device that has the part on reads them all, in every exchange. - Never in a line: a code or an approval (the line says a code came), a copy Android redacted, a picture (said), a blocked sender's messages, a conversation held for good, a notification its app marks secret, ongoing ones, Sioul's own, any button or tap (android.md): a reply from a computer names its notification by the phone's own name for it, and the phone finds that notification's Reply again (android.md).
- Padded: each line is padded with spaces inside its JSON to a multiple of 256 bytes before it is sealed (
phonemsgs::padded), so that the server learns no message's length; it still sees which phone wrote, when, and roughly how much. - A week: each phone takes its own lines out after 7 days (
phonemsgs::trim_own, once a day; its live lines too; each device its requests and outcomes,replies::trim_shared), and the sharing takes them out everywhere; another device's lines 10 days old are taken out by any device (phonemsgs::trim_others): a phone gone for good never takes its own out. - Remembered beside, no earlier versions, no first copy, as the calls' logs (
share::calls_storecovers both folders). - On a computer, in plain: the lines a computer received sit in its state folder unsealed, as the calls' do, readable by whoever reads your files there; the key that opens the folder's copies is on every device. That is the price of seeing them there, and the reason they are kept a week.
- An older Sioul knows neither folder: their records wait, written nowhere, taken out nowhere.
- Tested (
cargo test --release -p sioul-sync messages): a phone's message reaching the desk sealed and padded, the desk's Seen reaching the phone, what its notification offers a reply reaching the desk, remembered besidememory.json, no versions,files.jsonlost (a_phone_s_messages_reach_the_desk_sealed_and_seen_comes_back); a reply written on the desk reaching the phone sealed, decided once whatever is read again, its outcome back without its words (a_reply_written_on_the_desk_is_decided_once_and_its_outcome_comes_back); off by default, off on the phone sending nothing, off on the desk receiving nothing and keeping nothing waiting, then on (the_phone_messages_part_is_off_until_turned_on_and_off_receives_nothing); a line taken out by its phone a week on, gone from the desk's copy (a_trimmed_line_of_phone_messages_leaves_every_device); an older Sioul (an_older_sioul_keeps_the_phone_s_messages_waiting). What makes a line, the padding, the trims and the Porch's lines:cargo test --release -p sioul-core phonemsgs.
Texts¶
Your phone's texts, read there for your computers and kept there as an archive, and the texts written on a computer for the phone to send (texts.md), in their own part, Texts (texts), off until you turn it on, on each device.
- Three folders of lines in the data folder, one writer per file, every line sealed at rest by its writer (
textseal, a key made from the sharing key) and padded inside to a multiple of 256 bytes, then sealed again in the folder as every record is (share::TEXTS_LOG,TEXTS_SEND,TEXTS_OUTCOME,Shape::Lines): each phone's texts, the whole history, every column of the provider's rows, and a line per text deleted on the phone (or moved: its row's date rewritten by its provider, the line naming its new one,moved, which computers show once); each computer's texts to send, a random key each; each phone's outcomes. - The media of multimedia messages (pictures, sounds, videos, contact cards, other files) go through
blobs/as sealed, content-addressed blobs, never inside a line; each computer keeps them sealed indata/texts/media/<hash>. A part over the size chosen on the phone (10 MB by default) stays there. - A multimedia message written on a computer (
texts::Request:media, each a content's hash, its type and its size, never a file's name; its words inwords;bodyempty, so that an older phone's Sioul refuses it rather than sending its words alone): its media sealed intoblobs/before its request is written, and sent before it on the urgent lane (below, "Sent to the server too"), each asked of the server by its own name; the phone asks for one not there yet by its name (blobs::Wanted::media, which the exchange never prunes), and its own fetch brings it, a sync app or not. The phone keeps the message it builds, for Android to read, in its private texts folder (outgoing/<key>.pdu), never shared, gone with its outcome. Each phone's outcomes also hold, a line of no request's, what each SIM's carrier lets it send ("state": "limits", numbers only), when it changes and once a day. - Paced: the phone's import runs after the exchange, never inside it, 500 texts and about 24 MB of media a step; the doses, do-not-disturb and calls go first (
share::tests::a_first_import_goes_in_batches_and_never_holds_back_the_doses, anddoses_are_never_known_wrongly_whatever_carries_themwith a texts import beside it). - Kept: the texts always, an archive that only grows (
share::tests::the_texts_archive_only_grows_and_a_deletion_is_marked_everywhere); a device takes its own requests and outcomes out after 31 days, and another's 14 days later (texts::trim_own,trim_others). - Remembered beside, no earlier versions, no first copy, as the calls' logs (
share::calls_storecoversdata/texts/).files.jsonlost, the requests are read again, and the phone's ledger never sends one twice (share::tests::a_text_written_on_the_desk_is_sent_once_by_the_phone_and_its_outcome_comes_back). - Passed over: the records of the first build of the texts, never released, which kept them in
state/texts/, are known and written nowhere, never kept waiting (share::RETIRED); that build's reader starts again from the first text. - Never shared: the phone's ledger, its mark, the reader's marks and measures and Java's results stay on the phone (the state folder's
texts/); each computer's choice of the phone that sends and the phone's largest media size (texts.toml), and the clock's measure (texts/clock.json), stay on their device. - Tested (
cargo test --release -p sioul-sync text): a text written on the desk sent once by the phone, its outcome back, sealed in the folder and at rest,files.jsonlost; the part off until turned on; the archive growing, a deletion marked on every device.
Earlier versions¶
Before another device's change is written into a file here (any shared file: settings, lists, notes, papers), or takes it out, the file as it was is kept in $XDG_STATE_HOME/sioul/share/history/<part>/<file>/<time>, on this device only, never shared: by reference when its content is sealed in the sharing folder already, and there (a note or a paper a record named: a few bytes, <time>.sealed, kept 12 days, history::SEALED_DAYS, since the folder keeps that content two weeks after it was replaced, less a day at most: a version kept within a day of the change always opens; until 15 October 2026, 60 days against 90), else copied, when the disk has room for it (refused otherwise, and the change waits). A file another device took out is moved there, as a copy (history::keep_moved: no disk spent; copied when the move cannot be made), never kept by reference: its content may leave the folder as soon as every device read that it was taken out (above, "Old contents go"). Kept by reference rather than copied on every device that receives a change: copies would cost each device's disk up to the history's cap, a phone's too, and hold a change back where the disk is short. A content no longer in the folder is copied (a device back after weeks away keeps its own last version so); a change that reached this device more than a day after it was made may keep a reference whose content goes before its 12 days: putting it back then says the content is missing, and writes nothing. Of each file, the last 20 versions are kept, and all those of the last 30 days; of a file no longer here, those of the last 30 days; never more than a GB in all (or a twentieth of the disk if less), the oldest going first. Settings ▸ Your folder and sharing ▸ Show earlier versions lists them, part, then file (found by part of its name; a hundred shown, the others said), then versions. "Put back" says first what it would change, then puts it back, off the window's thread: a note, a paper, a draft whole (the file as it is then kept too), its version opened beside its place outside the exchanges' lock, in the deepest of its folders that is there, dated now while it waits, and the file as it is copied into its versions outside the lock too, closed there (history::prepare; re-check of part 3, R-7), the lock taken only to keep that copy by a rename while the file is as it was copied (its size, time and inode; else copied again outside the lock, up to three times, and only then under it: share::PREPARE_AGAIN, since 22 October 2026, when CI's Linux job saw a dose wait 0.49 s behind a 64 MB note copied under it; a test says why if it ever is, history::UNDER_LOCK) and put the version in place by another, and the window's exchanges never held back meanwhile (share::put_back, after_exchange; review of part 3, J2: until 20 October 2026 the whole version was opened under the lock, inside the window's exchanges' step that a dose's alarm waits for; a 24 MB note's version now: 0.54 s to put back, a dose's exchange waiting 0.1 ms at most, a_large_version_put_back_never_holds_the_lock_for_its_time; over a 24 MB note, 0.05 ms, over a 64 MB note, 1.3 ms, each copy into the history 400 ms slower: 15 and 24 ms, a_version_put_back_over_a_large_note_never_copies_it_under_the_lock); a file of entries (settings, lists, the doses' record, sessions) entry by entry: each entry the version held goes back to what it was, those taken out since come back, and those added since stay. Putting back never takes out, on any device, what was added after that version. What changed is dated now, and the next exchange sends it as a change made here. Notes and papers are kept this way rather than in before-sharing-<day>/ (they may be large). Your calendars and contacts are kept there too, sharing on or off, under their own heading: an event, a task or a contact changed here and on its server between two syncs keeps the server's version, and yours goes among the earlier versions (history::ACCOUNTS), to be put back from the window on any device, a phone too, whose state folder no file manager reaches; put back, it goes to its server at once, over the server's version. Of each item, the last 20 of your versions are kept, and all those of the last 30 days, whether the item is still on its server or not (until 10 October 2026, the sharing's hourly tidy took them for files no longer here and kept them 30 days only: share::correctness_tests::your_versions_of_an_event_stay_past_thirty_days). Until 10 October 2026 it went to $XDG_STATE_HOME/sioul/dav/conflicts, out of reach on a phone.
The log¶
- Each computer has a name: a UUID made once (
$XDG_STATE_HOME/sioul/share/here.toml), made again when the configuration was copied to another machine (a hash of/etc/machine-idtells). A disk copied whole, or a phone's state put back on another phone, copies that too: two devices then write under one name, each finding the other's lines in its own file, again and again, where a memory restored finds them once; it is said (share-twin), for you to stop sharing on one of them and start it again there, which gives it a name of its own. Each device notes where its own last append ends before making it (share/appended.toml; not noted, a full disk, nothing is appended:an_append_not_noted_is_not_made), so that its own lines, found ahead of a memory it could not save (a phone's process stopped mid-exchange, a full disk), are never taken for another device's (until 10 October 2026, one device stopped so twice in six hours was said to be two). Meanwhile the others meet gaps in that name's records, and the doses say the doubt (share::correctness_tests::two_devices_under_one_name_are_said,one_device_stopped_twice_is_not_said_to_be_two). - Changes are found, not announced: Sioul writes its files as before. Each minute (and on "Refresh everything"), the shared files are compared with what they held at the last look, entry by entry; a file whose size and time did not change is not read again. Each change becomes a record: the entry, its new value or "taken out", a clock.
- The clock is hybrid logical: the time in milliseconds, made strictly increasing on each computer and never behind a record already read. A change is dated by its file's time, so a change made here before another computer's, but found after, still loses to it.
- Merging: for each entry, the record with the later clock wins (ties broken by the computers' names). A setting changed on each computer keeps the later; two sessions noted apart both stay; a sender let in on one computer and blocked later on another ends blocked, and let in nowhere. A money line, split or preset, and a time session, has an id (given when it is made, or written into it when the device takes format 2): changed on two devices it stays one, the later change winning whole, and two alike stay two (format 2, below). A draft sent (taken out) on one device while another changed it, each unaware of the other, stays sent on every device, whichever came later: each record of a draft names the version it replaced, and each device keeps every version of a draft it met (
Memory::drafts), so that a change of a change of the version sent loses too; the change made meanwhile stays among that device's earlier versions (share::DRAFTS; until 10 October 2026 the later word won, and a draft sent could come back, to be sent twice:share::correctness_tests::a_draft_sent_never_comes_back_from_changes_made_meanwhile). An older Sioul's records of drafts name no version: the later word wins, as before. - Joining: on the first exchange, a copy of every shared file goes to
share/before-sharing-<day>/(notes and papers: their earlier versions instead), never written over: a first exchange tried again, or sharing stopped and started again the same day, keeps the files as they were before sharing (share::correctness_tests::the_copy_kept_before_sharing_is_never_written_over); the others' records come first; then what this computer alone had goes out. Two computers set up apart end with the first one's settings and the sum of both lists. - Prudence: a settings file missing or unreadable (half written, broken by hand) is never read as emptied: nothing of it is taken out elsewhere, and what comes for it waits, kept, until it can be written. So is a list or a settings file that is not text (a sender list kept by another tool in Latin-1): left as it is, said (
share-unreadable), never read with replaced characters, which would travel, nor written over with the incoming entries alone, which took its other lines out on every device until 10 October 2026 (share::correctness_tests::a_list_that_is_not_text_is_left_as_it_is_and_takes_nothing_out). A settings file that held entries and is found gone or of no bytes at all (a crash, a full disk, a file written in place) waits ten minutes before its entries count as taken out: never everything, everywhere, at once (share-emptied); a list emptied on purpose, or a file gone from a folder of files (a draft sent), is taken out at once. A file over 16 MB stays here. - Stores that leave, arrive or move: a store no longer shared here (a part switched off, a notes folder unset) takes nothing out elsewhere; a store new here (a part switched on, a store a new version adds) or moved (another notes folder) joins as a new computer joins: its files are rebuilt from all the records first, then only what they alone hold goes out, so an old copy here never beats the others' newer values and an empty new folder is filled. A store this version no longer has is left aside when a device on an older Sioul still sends it: the watch's (
data/watch/,state/watch-offers.json), taken out on 8 October 2026 (health.md, "Your watch"); its changes are neither written nor kept waiting, and the rest of the exchange goes on (RETIREDincrates/sioul-sync/src/share.rs). - The build that wrote them: the first line of each batch a computer appends (an exchange's changes, a new round's opening) carries, sealed in its change, the build that wrote it (
build,sioul_core::build::DESCRIBED: "0.0.3 (eff8661abcde)", building.md); the lines after it, until the next that says one, are that build's. Sealed: the server never learns it. An older Sioul passes over the field, and its lines, which say none, read as ever (share::tests::each_batch_of_records_says_its_build). - Records go to
<folder>/<computer>-<round>.jsonl, one line each, appended. Each computer writes its own file only, so the sync never makes conflicted copies. Each reads the others' from where it stopped (round and byte), whole lines only, each line on its own: a line that is not text or does not open (a disk's damage) is one broken line, said, and the next ones are read. Rounds: a computer's file past 32 KiB (share::SEGMENT) is continued in a new round of its own, nothing restated: a server sent the file whole at each change (WebDAV has no append) is sent a small one. Once as much as its last full round restated has been appended since, and a megabyte at least (ROUND_SIZE,full_round_after), a computer opens a full round, restating every entry it holds the last word on (on a computer, a megabyte or two; on a phone sharing its texts, its whole archive: 16 MB on the owner's phone, 14 October 2026). Until 15 October 2026 one opened at each megabyte appended, and a phone's full rounds cost the folder and the connection many times what it appended (eDrive sends each twice); now restating never costs more than what was appended since, and the rounds kept stay within twice the last full round (share::storage_tests::a_full_round_waits_for_as_much_as_the_last_one_restated). While its connection is metered or measured slow (share::set_frugal), the full round waits, within 4 MiB appended; while a bulk import goes on there (share::set_importing: a phone's first import of its texts, or one it fell behind in, said by the window's exchanges and the background step's alike), none starts at all, within 64 MiB appended, and the first exchange after the import opens one once the import appended more than the last full round restated (share::quiet_tests::no_full_round_starts_while_a_bulk_import_goes_on; on a phone whose last full round restated 16 MB of texts, an import of a few MB opens none; on 8 October 2026 a first import of 10 MB of texts made six full rounds, 63 MB in all, each sent twice). A computer's first round, holding all it had when it joined, counts as full. Each round says where it stands, sealed on its first line (share::Turn): it opens a full round, or it continues the round before, with the number of that round's last line. A reader enters a continuing round only once it met the round before to that line, read or found damaged (a damaged line is one line lost, said, never counted as read:Memory::met): a sync app may bring a round before the end of the one before (each file travels on its own), or before the whole round before, and that end, a dose marked taken among it, would never be read; until 10 October 2026 the reader moved on and took everything as read, so that a dose taken could show as known not taken. It now waits for that end, the doses in doubt meanwhile, or, if it never comes (an older copy of that round kept on the device for good), reads on from a later full round, which restates all. A round missing while an earlier round of its computer is kept has not come yet (rounds go from the folder oldest first): waited for, an hour of exchanges at most (share::AWAITED: each exchange counts the time since the one before, five minutes at most, so a device asleep or closed counts none of that time, and woken before its sync app brought the round, it waits on rather than ask for a full round for nothing:time_asleep_counts_none_of_the_wait); then, or missing with every earlier one (taken out), what it held past what was read is a gap, said, and the reader reads on: the rest of that computer's records flow, and the doses doubt what it marked until its next full round (a_round_truly_lost_is_passed_over_after_an_hour). A full round asked for: a reader that met another computer's records past what it read in order (a line lost, a round passed over, a device first read from a round continuing one gone) says so in its notes (Seen::full: that computer, and the latest round of its met), and that computer writes a full round at its next exchange once all its stores read (Memory::owed), but not while a bulk import goes on, and while its connection is metered or slow for six hours of exchanges at most (share::OWED_METERED, counted as the hour's wait above, time asleep left out), then all the same: on a phone on mobile data it restates every text it holds, but that phone marks most doses, and the others would otherwise say "check" on them for days; the doubt holds meanwhile (an_asked_full_round_waits_while_the_connection_is_metered,an_asked_full_round_goes_after_six_hours_metered); an older Sioul passes the request over, and its next full round comes as before. A device joining from a round that continues one gone knows where it stands at the next full round, with nothing said. A round is full only when it restates all: one written while a file of a store here could not be read (the doses' record held empty after a crash) continues the round before (the entries of a store not here, a part switched off on this device, are passed over, as ever, and stop no round from being full; those of a store that left,RETIRED, are forgotten:a_part_switched_off_stops_no_full_round,a_retired_store_s_entry_is_forgotten_and_stops_no_full_round), and after lost lines a full one follows as soon as everything reads again (Memory::owed). A device that finds a round of its own without a line ahead of its memory (its first writing lost) continues the round before it as that round ends, read from the folder (share::correctness_tests::continuing_rounds_brought_in_another_order_all_come,a_damaged_line_does_not_stop_the_rounds_after_it,doses_come_whatever_a_late_carrier_brings_across_segments,a_round_that_could_not_restate_all_is_not_full,an_empty_own_round_found_ahead_says_where_it_stands). A memory kept before reads every other computer's kept rounds again, once: what was known changes nothing, what was passed over comes in (share::correctness_tests::a_round_brought_before_the_end_of_the_one_before_loses_nothing,a_round_whose_end_never_comes_is_read_past_from_a_full_round,a_device_joining_from_a_continuing_round_says_nothing_lost,an_older_memory_that_moved_past_a_rounds_end_reads_it_again). An older Sioul's rounds say neither, and are read as before: a device's rounds are protected once that device runs a Sioul that writes the turn, and an older reader keeps the old way, so every device is updated the same day. An old round goes once every device that counts has read past it, and only one before the last full round: a device joining reads from the oldest kept, and finds every entry there (share::tests::a_change_sends_a_small_file_and_a_late_joiner_reads_everything,a_full_round_waits_while_the_connection_is_metered). Who counts (share::readers, 16 October 2026) is said by sealed facts, as for the newer form (share::present): a device that left, one you forgot, one another device's entry replaces, and one silent 180 days by its entry and its records hold nothing back, whatever their plain notes say; a device that counts and whose notes the folder does not show holds everything back (a hostile folder hiding them never makes a round go); a file of notes alone (a device whose entry has not come yet) holds back by its notes, as before: plain notes only ever hold more back. Until then every device whose plain notes were heard within 180 days held rounds back: on 16 October 2026 a phone dead for good kept the others' past rounds in the owner's folder, for half a year to come. The sending, which has no key, reads who counts from the memory, as the last exchange found it (Memory::counting,uncounted;share::rounds_past; a memory saved before, by the rule as it was, until its next exchange). The past rounds of a device that no longer counts (it left, you forgot it, another replaces it: a dead phone never takes its own out) go too, taken out by each device that counts, from its folder and from what it fetched beside it: only those below its last full round found in the folder (sealed on its first line: a hostile folder can hide a later one, never show one that is not), never that round nor those continuing it, and only once every device that counts read it past them (share::departed_rounds,Memory::departed). They are never fetched again, and a listing takes those still on the server out (below, "Sent to the server too" and "Kept in step by Sioul itself"). Any device that counts does it; two at once find the file gone (404) or changed (412), and leave it (share::rounds_tests::a_forgotten_device_s_stale_marks_no_longer_hold_the_others_rounds_back,a_device_that_left_or_was_replaced_holds_nothing_back,a_silent_device_holds_nothing_back_whatever_its_notes_say,a_live_phone_s_rounds_wait_for_every_device_that_counts,a_dead_device_s_past_rounds_go_never_its_last_full_round;remote::tests::a_dead_phone_s_past_rounds_go_from_the_server_too,a_dead_phone_s_past_rounds_go_from_the_server_in_the_mirror_too). A device can look departed while it lives, to a device that never saw its newer entry (one a server holds back from a device joining, a sync app bringing the rounds before the entry): that device then takes out its rounds below its last full round, and in time its contents since replaced, as the live device itself would; what goes is bounded by that full round, which restates all (review of 17 October 2026, L3). "Forget this device" is this device's own word: the others go on counting it until you forget it there too; "Does this phone replace…?" answered Yes is sealed in the new phone's entry, and every device reads it. Until 8 October 2026 a round was its opening plus a megabyte of growth: 2.5 MB on the owner's computer, sent whole at each change. Entries taken out are remembered 180 days, as long as a device may be away and still count (share::TOMBSTONE_DAYS, tied toSILENT_DAYS), and each full round restates them: a device back within that time, forgotten meanwhile or not, reads from a full round and takes out what went meanwhile (share::rounds_tests::a_device_back_after_ninety_days_takes_out_what_went_meanwhile). Until 17 October 2026 they were remembered 90 days, and a device forgotten, back after more than 90 days, kept for good a sender, a draft or a note taken out elsewhere (review of 17 October 2026, M1). The lines of the logs every device ages out by itself (the calls', the phone's messages', the texts' requests and outcomes) are remembered 90 days, past the longest any device keeps one of their lines (AGED_TOMBSTONE_DAYS): a line older than that is never shown, and goes by itself. A line's own words are its name, so what a full round restates of a line taken out weighs about what that line did: some 170 bytes for a sender, 190 for a draft, 610 for a phone's message, padded (a_full_round_restates_what_was_taken_out_half_a_year_and_aged_logs_ninety_days). A device back after more than 180 days, which no longer counted, keeps what was taken out more than 180 days before the others' last full rounds. Read a piece at a time (8 MiB), never a file whole: a line longer than any record (32 MiB; the longest, a file whole of 16 MiB, is about 30) is one broken line, said, and the next ones are read; a computer's notes, the seal and the claims are never read past 64 KiB (a larger one is none); the end of this computer's own file is read back from its end only, to mend a line left half written. - A file put back older: a sync app may put back an older copy of a computer's file (after a conflict, or keeping old content when only the version changed). Its writer notices its own file shorter than it left it and starts a new round with all it holds (
share-own-cut); a reader finding a file shorter than what it read, or a record whose number does not follow the last one, says so (share-other-cut,share-other-gap) and counts what that computer wrote after as not known (doses: a doubt) until its next round. A line left half written is cut before the next one is appended. - A memory restored or lost: a computer whose own records in the folder go further than its memory (restored from a backup, its state lost, sharing stopped and started again) goes on numbering after the folder's last line, never again from the start, and reads every file again from all the records, its own too, so what it marked after the backup comes back (
share-own-ahead); its memory keeps its entries, so what changed here since them goes out as a change made here. - The folder as set up: each exchange first looks at the sharing folder and its seal (
share::sealed_for). The folder gone (a disk not mounted, a sync app's folder moved): nothing is made, read or written, said (share-no-seal); until 10 October 2026 it was made again, empty, and what was written there later met the real files as conflicted copies. Its seal gone from a folder this device saw sealed (a sync app that made the folder again, an empty placeholder of a file kept on demand): the same. A seal there that no longer opens with this device's key (two devices that each sealed the folder before the other's seal came, the later one put in place of the first): nothing is written under a key the others lack, said (share-sealed-otherwise); sharing set up again there with the passphrase the folder's seal holds mends it: that device starts a new full round under that key, and the others, which could not open its earlier lines, read on from it. Another sharing than the one this memory knew (a seal of another salt: sharing set up again with another folder while the memory stayed, the keyring's key lost) makes the memory start anew, as when sharing stops and starts again; before, it counted as joined there, and what the device held never went (share::correctness_tests::a_folder_gone_is_never_made_again_and_a_seal_that_does_not_open_takes_nothing,two_devices_that_sealed_apart_meet_once_one_is_set_up_again,set_up_again_with_another_folder_the_memory_starts_anew). - Notes to each other:
<folder>/<computer>.tomlsays when each computer last exchanged and how far it read the others; written when what it says changes (its round, how far it read), else restated a quarter of an hour after the last time while the device is in use (its entry saysworking: another device's do-not-disturb hears of a computer in use by them), once a day while it is put away (a phone in the background: how far it read changes rarely;share::quiet_tests::a_device_put_away_restates_its_notes_once_a_day). Settings says when each other device was last heard of from the later of its notes and its entry (share::others_heard). - One exchange at a time on a computer, whoever runs it (the window, a dose's alarm, a second Sioul, the command line): a lock on
share/exchange.lock. A whole exchange finding another running tries again for a second, then does nothing (share-busy); a quick one waits for it. The second covers a program starting elsewhere in Sioul at that moment: it holds a copy of every file open then until it runs (a Mac's for milliseconds), the lock's among them. Hence, too, the host name is asked once per process (lease::host_name): on a Mac, which has no/etc/hostname, it once startedhostnameat every claim, three times a minute, each start holding the lock's file for a moment (found on 6 October 2026, by the tests on macOS). - Quick exchanges: a dose's alarm, the "Taken" button, the time notification's buttons and Sioul closing exchange without notes and papers, which stay as they are and wait for the next whole exchange (where each store was read is kept: nothing joins again). A quick exchange waiting tells the one running to hurry: its work on notes and papers stops where it is, and goes on at the next. A button pressed is applied here first, then sent: nothing slow can lose it.
- An exchange with nothing new (
share::exchange,quiet): an exchange without notes and papers (a phone's background step, a dose's alarm, a button) first notes how what it reads stands (Standing): the stores' files, each by its size, time and inode; the other devices' records in the folder and in what was fetched from the server (the pull writes there only what changed on the server), by size and time; every other name there by itself (the notes<computer>.tomlrestated); what each other device's entry says but for its exchanges' times (a computer in use restates its entry each minute); whether full rounds wait and whether this device is in use. As when the last exchange in the process left everything settled (nothing said, nothing waiting but for notes and papers, no part held), with its memory's two files as it wrote them, its own records' file of the size it left it, and nothing fallen due (the hour's tidying of the history, this device's notes, the folder's leftovers once a day), it reads nothing and writes nothing, and says what a whole exchange would: nothing sent or received, how far this device wrote, and its files looked at when their stamps were taken (whatever was written before is in its records). Any of those changed, or a file changed less than two seconds before (a coarse clock may hide a change at the same size and time), and the exchange runs whole. Measured on a computer with an invented folder the size of a phone's that shares its texts (6,000 sealed files, afiles.jsonof 15 MB, a texts' log of 10 MB:share_idle_bench.rs): 78 ms of CPU an exchange before, a third of a millisecond now; two hours of steps beside a computer in use, 3.4 ms each on average, the hour's whole exchanges among them. Through the whole exchange's tests and the simulations of format 2, run with every exchange allowed to take that way, 58,495 exchanges did, all passing (share::quiet_tests::an_exchange_with_nothing_new_reads_nothing_and_says_the_same). - Kept between exchanges in a process that exchanges every few minutes for days (a phone's background service:
share::set_keeping): the memory as it last wrote it (Kept), taken whilememory.jsonholds the same bytes andfiles.jsonis the same file (its inode, size and time), and each log of lines as last read through (LINES), read again once it changed. With 10 MB of texts they weigh about 30 MB there; an exchange with something new then costs about three quarters of one that reads both files again (on that folder, 64 ms against 85). A window's process reads them at each exchange. What each send asks first, the files this device sealed (own_sealed), is kept in every process by the stamp offiles.json, or read from that part of the file alone (share::quiet_tests::the_memory_kept_between_exchanges_is_never_older_than_its_files,a_log_of_lines_is_read_through_only_when_it_changed). - Files written by the sharing go under a hidden name of their own (
.<name>.<process>-<n>.sioul.tmp), made new, then renamed: two exchanges never write into one. This computer's name and sharing (here.toml) and its memory reach the disk before they take their name. One a crash left half written goes an hour after: in the history at its tidying (after a change received, or hourly), among the folder's sealed files once a day, listingblobs/reading thousands of names (share::quiet_tests::leftovers_among_the_sealed_files_are_cleaned_once_a_day; until 10 October 2026 after each change received).
The format of what travels¶
- Two formats. Format 1 is how Sioul 0.0.4 and every earlier version write their records. Format 2 (
share::FORMAT) names each money line, cover, mail rule, split and preset, and each time session, by an id of its own, and divides lists that format 1 sends whole (pinned sites, routines, task kinds, days off, the words you added to a list or took away, the lighter days) element by element: two devices adding to one list in the same exchange keep both. A device writes format 2 only once every device of the sharing reads it; until then it writes format 1, which an older Sioul reads. Each file's two ways of dividing are its rules and theirnext(share::Rules). - Ids. Sioul gives an id when a money line, a split, a preset or a session is made (
sioul_core::ids: 128 random bits, shaped as a UUID; pinned sites, routines and task kinds already had theirs). What an older Sioul made has none: a device taking format 2 writes one into each such element of its files, once (below). An element still without one afterwards (written by hand) is named by an id derived from the key format 1 named it by (SHA-256 of its list and that key, shaped as a UUID): a session by its start, task and project, a money line by what it holds; elements format 1 named alike stay one entry, as in format 1. - Each part says its format. Each record carries, sealed in its change, the format of its part (
f; absent: 1): settings, time, and projects and money are in format 2, the other parts in format 1 (share::part_format). An older Sioul passes over the field. - Who holds format 2 back (
share::format_holders). Each device's sealed entry says the format its Sioul reads (format, below, "Devices"); an older Sioul's says none. A device writes format 2 once another device at least counts (alone, it writes format 1: one joining later may run an older Sioul) and none holds it back. Every device known here by a sealed fact counts: its entry (the latest seen here, kept,Memory::seen: an older copy put back never lowers it), its records in the folder, or its records read here before (Memory::read_n,heard_at), whatever the folder shows now; but one whose sealed entry says it left (Stop sharing), one you forgot (Forget this device,share::forget_device) until it says anything newer, and one not heard of for 180 days. A file alone (plain notes, an entry that does not read) with no record is a stray file, not a device. Heard of is judged from sealed facts only: its entry's times and the clock of its last record read here (Memory::heard_at; a memory from before it was kept learns it from each device's last record in the folder, or, its rounds gone, from the latest of its changes this memory still holds; one of which no clock is known at all is aged from when this device first found it so,Memory::unclocked), never its notes in the folder, which are plain. A device that counts holds format 2 back when its entry does not read here, or it has none (an older Sioul, a device known by its records alone), or says an older format, or when what it wrote up to its entry is not all read here. A hostile folder can hold the switch back longer; it cannot open it past a device this one has read, even by hiding all its files. Settings ▸ Your folder and sharing lists every device that holds it back: one the doses do not count (known to the sharing alone) has a line of its own there, with Forget this device; one they count is offered it once silent for a week, as any device, since forgetting a device in use could make a dose it took look not taken. A device that meets a record in format 2 takes it from its next exchange: another device writes it already. A device joining takes it at once when every other says it reads it. -
The switch, once on each device (
switch_format, at the start of an exchange):- every file format 2 divides otherwise must read whole; one that does not (half written, broken by hand), or of no bytes while it held entries, keeps the device on format 1, said (
share-format-wait), and the switch is tried again at the next exchange; - what changed in those files since the last look goes out first, in format 1 and as format 1 names it (
flush): a removal or a corrected session made just before reaches the devices still on format 1; - each element of a list format 2 names by ids is given its id in the file, through the file's lock, the file as it was kept among the earlier versions first (
give_ids). The element that stands for a format-1 entry (what was last sent or received under it) takes the id derived from that entry, as every device holding it derives it; another (a second session at one start, task and project, a second line alike, a preset an older Sioul never carried) takes a random id, and goes out as new. Of several elements format 1 named alike, the one standing for the entry is the first when the entry's last word came from another device (format 1 wrote it there) and the last when from this one (format 1 sent that one). A preset's new id is put where splits and lines named it by its place (#3); - the memory is said in format 2's names, each renamed entry's last clock kept as its floor (
Floor), with, for a list format 1 sent whole, the elements it held and those a whole list took out here, with that list's clock (Floor::removed, noted after each exchange while the device wrote format 1,Memory::lists). An element standing for its entry but not holding exactly what was sent goes out again under its id.
The memory then says format 2. Stopped half way, the next exchange does it again from the files and the memory as they are: ids already written stay.
- every file format 2 divides otherwise must read whole; one that does not (half written, broken by hand), or of no bytes while it held entries, keeps the device on format 1, said (
-
Format 1 read after the switch (a device not switched yet, during the minutes its sync takes to carry the news; a device's own old records read again when a store joins, a record is rebuilt or an older copy of a round file comes back; an older Sioul back) is translated (
translate). A record at or below its entry's floor is passed over: nothing format 1 already said comes back. An element's change takes its id: its own, else for a session the id of the session of that start, task and project here (a device on format 1 writes a month back without the id the switch gave it here: the id is kept, and the session sent again from here under it when that id is its own rather than the one derived, so that a device joining later holds it once), else the id derived from its format-1 key. A value without its id takes the one it goes under, written into it: what the switch wrote stays. From an older Sioul (its sealed entry says an older format), a line's removal naming a version no device holds under that id any more (a copy changed since) is passed over; from a device whose entry says it reads format 2, and which still writes format 1 for the minutes it takes to see every other's entry, the later word wins, a removal too. The versions of one element in one reading (an older Sioul changes a line by adding the new one and taking the old one out) make one change: the latest held, at the latest clock. A list sent whole sets what it holds, and takes out, of what the previous whole list held, what it no longer holds: never an element added since in format 2. - A whole list's removal stays (
below_whole_floor): a record of an element of a list format 1 sent whole (a pinned site, a word, a day off, a routine, a task kind, a lighter day) at or below the clock of a whole list that took it out here is passed over. What a whole list took out is read from the whole lists' records themselves, each at its clock, in whatever order they come (WholeList: for each element, the latest list that held it and the latest that did not; a device joining reads both in one exchange), and from the file between exchanges while the device writes format 1, as when a device restates its old entries in a new round after an older copy of its round came back (share-own-cut): an element taken out just before the switch never comes back on one device only. An element no whole list here ever held (added in format 2 elsewhere, its record waiting here) is never passed over: a list that did not know it did not take it out. - What an older Sioul does meanwhile (0.0.4 and earlier, such as a phone not updated yet): it reads and writes format 1 as before, since no device writes format 2 while it counts. The fixes of format 2 wait until it is updated: until then, one session, the pinned sites or one list of words changed on two devices at the same moment keep only the later change, and two lines alike that it made become one. Lines, splits, presets and sessions made meanwhile by a newer Sioul already carry their ids, so that two alike stay two; an older Sioul keeps a line's id, and writes a month of sessions back without theirs, which costs nothing (format 1 names sessions by their start, task and project). Settings ▸ Your folder and sharing says that a device runs an older Sioul and holds the newer form back (
share-format-held), on that device's own line. Retired without Stop sharing, it holds format 2 back until you choose Forget this device (offered once it has been silent for a week, or at once for a device the doses do not count), or for 180 days. - An older Sioul back after the switch (silent 180 days, forgotten, or put back by hand) cannot be taught: it reads format 2 wrongly on its own screen (a line changed elsewhere added a second time there, a list's change failing to write there). What it sends stays format 1 and is translated by the others with the floors above: a version of a line it takes out that the others changed since is passed over, and its whole lists take out only what their previous whole list held. Its other changes are taken as changes, the later word winning. The panel says so (
share-format-older) while it counts; updating it there mends it. - A newer format met (records of a part in a format above this Sioul's:
share-newer): they wait (Memory::waiting), never written into the files here; nothing of that part goes out from here: what changes here in it stays in its files, with when it was found (Memory::held). The records of formats this Sioul reads, of that part too, are still read. It is said once in the status line, and in Settings ▸ Your folder and sharing as long as it lasts, with the build that wrote it. Once this Sioul reads that format (updated), the waiting records are written and what changed here goes out at the time it was found, so that a later change of the newer device still wins over it, and an earlier one loses. Once the devices that wrote it no longer count (present, by sealed facts: left, forgotten, silent 180 days, such as a test build tried once), the part is released: what waited of it in that format is dropped, what changed here goes out, and those records read again are passed over (Memory::released). Nothing else releases it: an entry saying an older format (its records ahead of its new entry, or an older copy put back) does not. For the doses' part, while it is held, the newer device's dose records are not counted as heard (share::heard), this device's exchanges claim no export (Outcome::looked0), its entry says its doses are not shared (dosesfalse) and it never closes its entry as if all its answers went out: the hold is read from the memory on the disk (share::holds), the same after a restart, in any process. The others then doubt a dose this device may have answered, never take it as not taken (docs/health.md, "Knowing"). - What stays as it was: lists edited whole on one page, or whose order counts, still travel whole, the later winning: working hours and office hours (a row per day), mail filters (their order decides), the hours' categories, the helps of a pause, the times of the gathered reminders, the projects hidden from the Porch, the attention's rows, the meals and naps (named by their place,
meal:1). Ties (links.toml), the mail said to be no payment and the tasks set aside today are named by what they hold, as before. Putting back an earlier version (above, "Earlier versions") puts back each element by its name, and takes nothing out that was added since. - Tested (
cargo test -p sioul-sync format_testsandswitch_tests, the fixtures incrates/sioul-sync/tests/fixtures/format-1/, made by Sioul 0.0.4 before format 2 was written): two lines alike made on one device stay two through two devices and a third joining, and two made by 0.0.4 reach the other device once the switch gives the second its id (two_identical_lines_made_on_one_device_stay_two); one session changed on two devices at once stays one in format 2, one 0.0.4 noted without an id too (the switch writes it its id), and doubles in format 1 (one_session_changed_on_two_devices_stays_one,a_session_without_id_changed_on_two_devices_stays_one); two sites pinned and two words added at once on two devices all stay, a word taken away on one while another is added on the other both hold (sites_and_words_changed_on_two_devices_at_once_all_hold); a sharing written by 0.0.4 read while the phone still runs it, then both switching, nothing doubled or lost on the desk, the phone or a device joining afterwards (an_older_sharing_is_read_then_upgraded_without_doubling_or_losing), the phone's 0.0.4 records arriving after the switch (an_older_device_s_write_after_the_switch_doubles_nothing); a newer format read, never written over, said once (a_newer_format_is_read_never_written_over_and_said_once); format 2 waiting for every device (format_2_waits_for_every_device); lists divided element by element within tables (format_2_divides_lists_element_by_element_and_writes_them_back); and the review of 9 October (R1 to R14, L1 to L4), each finding a test of what now holds: a memory put back from before the switch, a whole list from a device not switched yet, a word taken away or a session corrected just before the switch, an older Sioul taking out a stale copy, a hostile folder, a held doses' part, held changes against a newer device's later ones, a part released once its writers are gone, a device joining after a translation, records read again after the switch (an older copy of a round, the doses' record repaired, notes switched on), an earlier month put back, a companion that left, a device forgotten, an older device said only while it counts, a file that does not read, and the switch under eDrive (size only, never deleting) with one device editing, then both, the phone updated at once, at minute 20 or at 45; and the second review (S1 to S9,share_switch_more_tests.rs): a device known by its notes alone, or read before this version and silent since, holding nothing back; a newer device's records ahead of its entry keeping the part held; a format-1 change of one session leaving its twin; a split naming a preset by its place ending the same everywhere; a session corrected in the mixed window keeping its id, then moved to another project on one device and corrected on the other, staying one; a hostile folder hiding every file of a device keeping the switch shut; a switch stopped half way; a later removal from a device that reads format 2 taken everywhere; 360 simulated upgrades under eDrive (40 seeds, three lags, the phone updated at once, at minute 20 or 45) and 150 with a third device joining mid-switch, all converging. And the third review (T1 to T9,share_switch_third_tests.rs): the Words tab and the days-off editor keeping a word and a day another device added while they were open, and the days' ids; the window and the sharing writing one month at once on two threads; a device read long ago whose files are gone stopping to count; a site unpinned before the switch, and one restated after a round cut, staying unpinned; the lighter days emptied keeping another device's; 570 more simulated upgrades, two and three devices, all converging. And the fourth review: a device joining in the middle of the switch keeping a site unpinned before it (a_device_joining_mid_switch_keeps_a_site_unpinned_before_it), 240 more three-device runs, the whole lists read in any order, one element changed on two devices staying one, an undo putting back only what it changed. These counts are the wide sweep's, every seed the reviews ran, whichSIOUL_SWEEP=wideasks for and Linux's tests on GitHub run; elsewhere, and by default, each simulation runs its first three seeds and the seeds that once found a bug (switch_more_tests::seeds; building.md).
Sent as soon as it is saved¶
When a file a sharing part carries is saved here, whatever saved it (a dose answered, a setting, health.toml, a project, a budget, a draft), an exchange runs a few seconds later, then this device's files go to the server (Sioul keeping the folder itself, or beside the sync app: "Sent to the server too", below). One mechanism, in crates/sioul-sync/src/trigger.rs and the window's share.rs:
- Seen, not announced: every two seconds the window looks at the shared files' sizes and times (
trigger::Stamps, a few hundred files: the notes and papers folders left out, their thousands of files read by the minute's exchange as before; hidden files left out: locks, files half written). A change, by whatever wrote it (a page, the command line, another program), is a save. Whatever was written while an exchange ran (the exchange itself, what runs before it, the minute's other work) is taken in after it, never taken for a save: a change it did not carry goes at the next one. Nothing is looked at while an exchange runs. - One worker: every exchange of the window runs on one thread (
share::worker), when it is due (trigger::Soon): asked (a button, a dose answered, a switch, do-not-disturb, the minute's tick:share::exchange, as before), at once; saves seen, two seconds after the last, five after the first at most, so that a page saving a field at each key sends once, and soon; but never less than 50 seconds after the last exchange began (trigger::MIN_GAP_MS), and the minute's scheduled exchange is left out when one began less than 50 seconds ago (Soon::tick,share::exchange_tick): a file Sioul writes each minute (a session's time, a plan) costs one exchange a minute, not two, and a quiet minute only the scheduled one. A save is thus carried within the minute, within seconds when the last exchange is older. What is asked or saved while one runs makes the next one; before, an exchange asked while another ran was dropped until the minute's tick. Never on the window's thread. - Then sent: after each exchange, what it wrote goes to the server (Sioul keeping the folder itself, or beside the sync app when the backup found the folder there), then a phone's sync app is asked to look (
CARRIERS): after, not before, so that it finds the server as the files here. A phone whose own pull from the server works asks it nothing, whichever runs the exchange: its background service, a dose's or a waking's alarm, its window (android.md). - On a phone, the window's process looks only while Sioul is on the screen. The processes that write in the background exchange themselves, then send, as before (
share::exchange_here: a dose marked from its notification, an alarm, the time notification's buttons, the background step): a button pressed sends off its thread (Android waits eight seconds for its answer); an alarm and the background step wait for it. - Tested: the pause and its bound (
trigger::tests::the_trigger_waits_for_a_pause_and_never_long); a quiet minute running the scheduled exchange alone, a file saved each minute costing about one a minute whenever in the minute, never two closer than 50 seconds (a_quiet_minute_runs_the_scheduled_exchange_alone); a save seen in any shared file, never in a note, a lock or a file half written, and what the exchange wrote never taken for one (trigger::tests::saves_are_seen_in_the_shared_files).
What the sync app must do¶
Very little, so that any sync app carries the folder: Nextcloud and ownCloud, Murena's eDrive, Syncthing, Dropbox, Google Drive, OneDrive, iCloud Drive, or another; or none, Sioul keeping it in step with a Nextcloud itself (below, "Kept in step by Sioul itself"). The sharing asks only that new files and files that grow are copied some day, in any order, maybe late, maybe never deleted, maybe with a conflicted copy beside them.
- Nothing is rewritten in place at the same size: the records only grow; the sealed notes and papers (
blobs/<name>) are new files, written again only when gone or cut (or made fresh, now and then, when a content sealed long ago is used again); the few files rewritten (the notes to each other, the claims, the devices' entries) change size at each writing, one step past the last, over a turn of several hundred writings (share::pad_for). Some sync apps tell a change by its size alone: eDrive brings a file changed on the server only when its size differs from the phone's copy (itsFileDiffUtils.getActionForFileDiff), others compare size and time. - Half-written files never travel: files are written under a name starting with a dot (which sync apps leave out), then renamed.
- Conflicted copies, temporary files, files of other names are never read: only
<id>-<round>.jsonl,<id>.toml,leases/<part>/<id>.lease,devices/<id>.device,blobs/<name>andseal.tomlcount. Two devices sealing the same content may both write itsblobs/<name>: either copy opens to it. - Deletions are not needed: eDrive never deletes a file deleted elsewhere; an old round left behind is never read again, an old sealed file never opened again. Where Sioul sends this device's files to the server itself, it takes its own past rounds and the sealed files it swept out there, whatever the sync app sent again (below, "Sent to the server too").
- Files kept on demand (OneDrive's, iCloud's and Google Drive's online-only files, Nextcloud's virtual files): a file not downloaded yet reads as empty or not at all, which is a file not here yet; the doses say what is not known meanwhile. Keep the sharing folder "always on this device" in those apps.
- Timing is the sync app's: the desktop's apps bring a change within seconds; a phone's often look every half hour. On a phone, Sioul asks the sync app to look now when it offers a way (
CARRIERSincrates/sioul-app/src/share.rs: eDrive's "force scan" today; Syncthing, FolderSync and others can be added the same way), after writing, when Sioul comes back on the screen, and every five minutes while it is shown. While Sioul's own pull from the server works (below), it asks only after writing, and only when Sioul does not send this device's files there itself; back on the screen, an exchange at once pulls and reads what came instead, with no twenty seconds' wait. Without one, changes come at the sync app's own pace, and the doses say what is not known until then: nothing depends on it but how soon. Where the folder lives on a Nextcloud one of your accounts reaches, Sioul also reads the others' files there itself, and sends this device's own (below, "Fetched from the server too", "Sent to the server too"). - Tested against each kind of carrier: a simulator copies the folder between a phone and a server as each kind does (every change both ways; size only; late and out of order; older copies put back with conflicted copies; files on demand); doses are never taken as known when they are not, and every dose comes in the end (
doses_are_never_known_wrongly_whatever_carries_them); notes travel file by file whatever carries them, a change and a deletion both ways, nothing of them readable in the folder (notes_travel_file_by_file_whatever_carries_them).
Fetched from the server too¶
When the sharing folder lives on a Nextcloud server (Murena's, ownCloud's) that one of your contacts-and-calendars accounts reaches, Sioul also reads the other devices' files there, over WebDAV, beside the sync app: a backup; and sends this device's own files there too (below, "Sent to the server too"). The synced folder stays the way; a sync app late or stuck (eDrive, 6 October 2026: a computer's records an hour and a half late on the phone, its entry among the devices never) then delays nothing. Code: crates/sioul-sync/src/remote.rs; the window's side in crates/sioul-app/src/share.rs (fetch_from_server).
- Found: on the accounts whose server address holds
/remote.php/(Google's never), in each one's files,…/remote.php/dav/files/<user>/, the user as its principal names it (current-user-principal). Where in them: the place given by hand (Settings ▸ Your folder and sharing), alone; else the folder's path from a phone's storage (eDrive carries/storage/emulated/0/as the cloud's root:Documents/Sioul), from the Nextcloud app's own folder, from a folder the Nextcloud client carries (localPath→targetPath,nextcloud.cfg), then its last one, two and three names. Looked for again every six hours while not found, at once when asked. - Confirmed only where the server's
seal.tomlis the folder's, byte for byte: a folder sealed otherwise is never read. The seal is read again when its ETag changes there, and every six hours; another one, or none, and nothing more is fetched. - When (
remote::due), before an exchange: the first pull as soon as the folder is found, whatever the hour: until then this device knows the others only from the stale folder, and cannot tell whether one is in use (6 October 2026, 23:30: found on the phone at night, then never pulled, the desktop in use taken for closed since 17:39). Then once a minute on a computer, on a phone with Sioul on its screen, or while another device is in use (as read here, what was fetched included); otherwise at the phone's background step's pace, five minutes, fifteen at night: a device opened at night is noticed within those fifteen. A dose's alarm, a waking and the background step asking for news pull twenty seconds after the last. Their pull working (switched on, confirmed, a pull gone through and the last one too, nothing left there that only the sync app brings:State::pulls_well), the background step asks the sync app nothing and waits for its pull alone, twenty-five seconds at most, as long as it had before, and no longer than it takes. A dose's or a waking's alarm, the folder found there, tries a pull and trusts it alone only once a pull begun since it went off went through (remote::alarm_news,State::went_through_since; this process's own pulls judged by its steady clock, another's by its time within the alarm's start and now, so that a clock set back never makes an older pull count): another one running in the process (a second alarm's, the window's) is waited for, never passed over, and one begun before the alarm makes it pull again; that pull failing, or not through within five seconds, the sync app is asked as before and waited for, its twenty seconds ended as soon as a pull since the alarm goes through. No pull through since the alarm (AlarmNews::SyncApp { heard }), the doses say the doubt for each device whose entry or claim on the doses says nothing more since (by its own times: another part's claim, a file touched, an older copy put back are no news), known closed only by what was read before (health.md). A pull tried "later" than now (the clock set back) never makes the next one wait (due). The window on a phone's screen no longer asks the sync app to look every five minutes either, its minute's exchange pulling. Otherwise the sync app is given its twenty seconds, as before (remote::tests::a_doses_alarm_on_its_pull_knows_what_it_knew_with_the_sync_app_asked,two_doses_at_once_the_second_alarm_waits_for_the_first_ones_pull,an_alarm_while_the_windows_pull_runs_pulls_again_after_it,a_doses_alarm_whose_pull_fails_asks_the_sync_app_and_never_knows_wrongly,an_alarm_whose_pull_does_not_go_through_in_time_asks_the_sync_app). A background step that asks the sync app nothing pulls at its pace. Never on the window's thread; one at a time. Each look and each pull is said in a phone's log (adb logcat -s sioul: "backup: 23:52:10 pulled from murena.io: 14 listed, 3 fetched, 2468 bytes", or why not), never a password, a name or a content. - What:
PROPFIND(Depth: 1) on the folder,devices/,leases/and each part's claims; a folder whose ETag did not change is not looked through again for ten minutes (a Nextcloud folder's ETag changes with anything below it). Of the other devices' files, only those newer than both the synced folder's copy and what was fetched before come down: records (<id>-<n>.jsonl) when the server's is longer, from the end of the longer copy here, its last whole line asked again (Range) and checked to be the same there, else the whole file; notes to each other, entries and claims whole, when their ETag changed, kept only when they differ from the synced folder's copy. Sealed notes and papers (blobs/) are never listed: only those a note or a paper waits for come, by their names (below). Never this device's own files. - Sealed files fetched when waited for (
remote::fetch_wanted): a note's record comes with the pull, its sealed file only with the sync app, at its own pace. On the owner's phone, 10 October 2026, notes written on the desktop arrived some 35 minutes late: once Sioul stopped asking eDrive to look at each step (to spare the battery), the sealed file came at eDrive's half hour, and the note waited meanwhile. Now an exchange that finds a note's or a paper's sealed file neither in the folder nor fetched beside it says so: in its outcome (Outcome::waits, "no-blob", in a phone's log: "sharing: 0 sent, 0 received, 1 waiting (no-blob 1)"), and in a small file of this device's (share/wanted.toml, never shared: each file's name, its content's hash and size). Right after it, the window fetches those files from the server by their names (GET blobs/<name>; never a listing ofblobs/), beside the synced folder (share/remote/blobs/), on a thread of its own (a large paper on a slow line never holds back the next exchange), then asks another exchange at once, which writes the note: within one step, the pull and the exchange (remote::tests::a_note_waited_for_is_fetched_by_its_name_and_written_within_one_step). A phone's background step fetches them too, after its exchange, beside the mail, waiting twenty-five seconds at most for them, for the window to write. Nothing is fetched while nothing waits. At most 16 files and 16 MiB a step (the first whatever its size), the smallest first; on a metered or slow connection, a paper's of 1 MiB or more at most every ten minutes, as large files are sent, a note's at once (sealed_files_waited_for_come_bounded_and_paced). Each is kept only once it opens whole to its content (blobs::check): one damaged there is asked again after a minute, then two, five, a quarter of an hour, up to an hour; one not on the server yet (its writer's sync app has not sent it) within five minutes (a_damaged_copy_fetched_is_refused_and_fetched_again,a_sealed_file_not_on_the_server_yet_is_asked_again_within_minutes). Reading a sealed file (blobs::get: the notes and papers received, the texts' media, a version put back) opens the folder's copy, else the one fetched; one fetched that does not open is taken out, and fetched again. A copy fetched goes once its note is written, once the sync app brings the same into the folder, after three days whatever, and all of them when the backup is switched off (a_sealed_file_fetched_then_brought_by_the_sync_app_is_kept_once,the_sealed_files_fetched_are_pruned). Doses never wait for it: a dose's alarm fetches nothing, and the window fetches only after its exchange, outside it. - Two processes pulling (the window's or an alarm's, and the background step's) never undo each other: a pull writes what it found over the state as it is then, under its lock (
State::save_pulled): the pull begun later says when one was tried and how it went, the latest that went through is kept (a time later than the pull's end dates from before a clock set back, and gives way), and what one found left to the sync app (new, or found again at another size) is never erased by the other's save, only cleared by a look that finds it brought (remote::tests::two_pulls_at_once_keep_what_each_found,a_round_found_again_grown_stays_left_in_the_merge). - Where:
$XDG_STATE_HOME/sioul/share/remote/, the folder's paths mirrored; what the backup knows inshare/remote.toml(the place found, the account by its id, ETags and sizes, when, why not; your switch and the place you gave), never shared. Never the password: it comes from the keyring for each look, as the calendars' sync has it, and goes to the server only, over TLS (plain HTTP only to a test's own stand-in). - Read: an exchange attaches what was fetched when its
seal.tomlis the folder's (remote::attach); then, for each other device's file, the newer copy is read: records the longer (they only grow, and each line starts at the same place in both, so where each device was read stays true), notes to each other the laterat, an entry by its times (each writing sets one to its device's clock: a start, a close, an exchange), with a phone's words (acked) only on the computers both copies name (devices::chosen: a computer either copy no longer names is withdrawn, one naming an older opening is only behind; the re-check before 0.0.6, LOW-2), a claim the laterrenewed. The synced folder's copy wins a tie. The doses' knowing reads the same (devices::all,lease::claims,share::others), sodoubts_nowsees what the server holds. - Never half: a file goes into the cache under a hidden name, then is renamed, once all of it came (its length said, or chunked to its end); records keep whole lines only, a line cut on its way coming again next time. A records' file cut on its way is kept up to its last whole line: a slow phone's next look goes on from there.
- Gone: what the synced folder caught up with leaves the cache (records as long there, entries and notes the same there, sealed files of the same size there). Records longer than the synced folder's stay until it catches up, even switched off: read past its end, a records' file gone would read as cut, and the doses would doubt for hours. Switched off, the entries and notes fetched go at once, and nothing is asked of the server. Sharing stopped, all of it goes.
- Failures: a server's error, a timeout (ten seconds for each wait, thirty for a look), a refused password end that look, said; the exchange runs as without it.
- Too large for a pull: another device's records longer there than here and past 64 MiB (
remote::LARGEST) are left to the sync app, and said: the look's log counts them, and while one is left the pull is not trusted alone (State::left,pulls_well): the background step and the alarms ask the sync app as before, until it has brought them, and Settings ▸ Your folder and sharing says so under the backup's line ("Some records of your other devices are too large to fetch directly: your sync app brings them, and Sioul asks it to look until it has.";remote::tests::a_round_too_large_for_the_pull_is_said_and_left_to_the_sync_app). A computer's file grows by one exchange's records past 32 KiB (share::SEGMENT), so only a full round restating a very large sharing reaches that size. - Said in Settings ▸ Your folder and sharing: "Also fetched directly from cloud.example.org, last at 22:41.", or why not (not found, sealed otherwise, a refused password, no such account), with the switch (on unless switched off here) and a field for the folder's place there.
- Costs: while another device is in use, a minute: three to six listings of a few kilobytes, its new lines, its entry and its claims (some hundred bytes each); nothing changed, one listing. One connection a process (
dav::shared_agent, kept fifteen seconds idle): a pull and the sends after it go through it, one TLS handshake where each opened its own until 9 October 2026; each request keeps its own limits (stalls::ask). Tested:remote::tests::a_steps_requests_go_through_one_connection. - Tested (
cargo test --release -p sioul-sync remote), against a stand-in WebDAV server in the tests (a folder on the disk served as a Nextcloud's, faults on demand): a stale phone folder caught up (do-not-disturb, the desktop's entry,doubts_nowas with a fresh folder; nothing written into the synced folder, nothing butPROPFINDandGETasked, never this device's files); a fresh folder unchanged, byte for byte against the same device without it; a file cut on its way never read as whole, each line read once; a seal that differs refused, and a seal changed here dropping what was fetched; server errors and timeouts leaving the exchange as without it; a fetched copy never winning over a newer synced one; doses never known wrongly with a sync app that brings nothing down; found at night, the phone's folder stale, the first pull at once, then the pace of who is in use (found_at_night_the_first_pull_comes_at_once,the_pace_of_pulls); where the folder is looked for.
Sent to the server too¶
Where the backup found the sharing folder on a Nextcloud (above, "Fetched from the server too"), this device's own files go there too, sent by Sioul itself over WebDAV beside the sync app, both ways then: the others' fetched, its own sent. The sync app stays the way; Sioul no longer waits for it to carry what this device wrote (a computer's records an hour and a half late on the phone, 6 October 2026), and a server that lost files is given them again ("Send everything again"). Code: crates/sioul-sync/src/remote.rs (send, Sender), the window's side in crates/sioul-app/src/share.rs (send_to_server, send_again).
- Only this device's own files: its records (
<id>-<n>.jsonl), its notes to the others (<id>.toml), its claims (leases/<part>/<id>.lease), its entry (devices/<id>.device), in that order; then the sealed files it made itself (share::own_sealed: their names and the size it sealed each at, from its memory), whole only: one being written, or of another size, waits. Never another device's file, never another device's sealed file the sync app brought, neverseal.toml. - Over what is there as it was seen:
If-Matchthe ETag it had after sending, orIf-None-Match: *where none was there. EachPUTsays the file's time here (X-OC-Mtime) and its SHA-1 (OC-Checksum), which Nextcloud keeps and shows to clients. - Records only grow: a records file is never sent shorter than what went, nor over a longer one there. One a sync app cut here (an older copy brought down) stays as it is there, remembered at the server's size: what is appended here later, while still shorter, never goes over it either; the exchange opens a new round (
share-own-cut). - Its texts and replies to send first, on a lane of their own (
remote::send_urgent, its own lock andshare/sending-urgent.toml; its replies to send the same way as its texts,phone-messages/ask/<id>.jsonl,remote::place_asks, where the part "Messages from your phone" is shared, which a phone's step reads beside the records' copy:remote::tests::a_reply_to_send_goes_on_the_texts_lane): this device's texts to send are copied into the folder as a small file of their own,texts/send/<id>.jsonl(remote::place_texts, the lines sealed already, written only where the part "Texts" is shared), and sent before the rest, and right after a text is written, never behind a large file going up on a slow line. The others' pull brings that file (texts/send/, up to 4 MiB), the sync app carries it too, and a phone's decision reads it beside the records' copy, each request by its key, once (Texts, below). Sioul keeping the folder itself sends it the same way. A multimedia message's media go up first on that lane, before the request that names them, each sealed file asked of the server by its own name (never the whole ofblobs/listed there), and forgotten by the lane once their quarter of an hour is over (texts::media_to_send;remote::tests::a_request_s_media_go_up_before_it_on_the_urgent_lane). - Given the time its size asks: each file a minute, and as long more as it takes at 16 KiB a second (
remote::FLOOR: a 2.5 MB file, 60 s and 156 s more; a phone's 3G uplink is faster); cut when nothing moved for half a minute; the wait for the server's answer, while the last bytes drain from the system's buffers, within that time alone. A send is not cut at thirty seconds as a pull is. Until 8 October 2026 a file had a flat minute: the owner's records, 2.5 MB on a phone's hotspot, never went, tried again from zero every minute. - A file that does not go (too slow, cut on its way): said in detail (
Failure: which file by kind, how, how long, how many bytes were handed to the network, of how many), insending.toml, in the log and in the panel ("This device's records did not go at 19:24: 2.5 MB did not go through in 216 s: slower than Sioul waits for. Tried again at 19:25."); held back, never tried before its backoff (a minute, two, five, a quarter of an hour, half an hour, an hour), the records after it with it (records go in order), the other files still going. The server unreachable, a certificate or a password refused: nothing is tried before the backoff ("Send everything again" apart). - Paced on a metered or slow connection: a file over 256 KiB goes at most every ten minutes when NetworkManager says the connection is metered (or guesses so), or when the last large file went slower than 64 KiB a second (
Sending::slow; a phone's hotspot may not say it is metered); the full rounds wait meanwhile (above, "The log"). A connection whose cost is not read yet after a start counts as metered: the window's first exchange waits for that first reading two seconds at most, once (share::metered,power::read_once_within), and none come by then, it goes as on a metered connection (review of battery part C, L3). - When: right after each exchange of the window (each minute, and a few seconds after a save, above, "Sent as soon as it is saved"); after an exchange from a phone's background, before its sync app is asked to look (its pull working, it asks it nothing); at the window's close (a computer waits five seconds for it); when sharing stops (its entry saying it left). Only what changed here since it went (its size and time): with nothing new, not a request. Every six hours, and the first time, the server's folders are listed (the folder,
devices/, each part's claims,blobs/) and each file compared with what is there: one lost there goes again. - The sync app sending the same file after: found the same there, nothing more. Its upload gives the file a new ETag (Nextcloud gives one at every
PUT, the same bytes included); the next send of a file changed here meets412, looks at the server's copy (PROPFIND: size and checksum), and when it is the file here (the sync app sent it first) takes it as sent, with no second upload: a sealed file by its size (its name is its content's), records by their size and their checksum when the server keeps one, the rest by their checksum, else read there and compared. Otherwise it goes over the copy there,If-Matchits new ETag. - What the sync apps do with it (read in their sources, 8 October 2026: eDrive
mainof 31 August 2026, the Nextcloud desktop client 34.0.4, Nextcloud server 33.0.8):- Nextcloud's server keeps the
X-OC-Mtimegiven and theOC-Checksumas sent (aPUTwithout one clears it; a plainPUT's checksum is never checked against the bytes), shows it inPROPFIND(oc:checksums), and gives a new ETag at everyPUT. - The Nextcloud desktop client, finding a file changed here since its last sync and a new ETag there, takes it for a conflict, then compares: the same size and a checksum it can compute (SHA-1 among them) and the same hash, it takes the server's as its own, nothing downloaded; else it downloads and compares the bytes: the same, nothing more. Only when the bytes differ does it rename the file here "(conflicted copy …)", which it never sends up, and put the server's copy in its place. With Sioul sending right after each write, the server holds the file here when the client looks.
- Murena's eDrive sends a file whose time here is later than the one it kept,
If-Matchthe ETag it knows in Documents (a "media" folder there), blindly elsewhere, withX-OC-Mtimeand no checksum; refused (412), it holds that file back an hour, then two, then five, and after a fourth refusal leaves it until it restarts, never making a conflicted copy. It brings a file down when the ETag there changed and the size differs from the phone's; the same size, it keeps the new ETag. It looks at the server first, then the phone; a folder whose time did not change (a file appended in place) is not looked at on the phone side, and a download planned then goes ahead over the phone's newer file. So after Sioul's send its upload of the same file is a second copy of the same bytes (harmless), and Sioul asks it to look only after sending. - What is left: the server one version behind the file here (Sioul's send of a write a moment before landed, the next write's not yet) at the very moment the sync app looks. The Nextcloud client then puts the server's copy back here and keeps the newer one beside as a conflicted copy; eDrive may bring the older copy down over an appended file. Never anything lost: the device's own records cut here start a new round with all it holds (
share-own-cut), its entry and claims are written again from its own memory at the next exchange, the server's copy never shrinks (noPUTover a longer copy, everyPUTover what was seen), and the others read the server's copy, which only grows: a doubt at most, never a dose known wrongly. The conflicted copies stay on that computer, never read ("Conflicted copies… are never read"), to delete by hand. Sending right after each write keeps that moment short.
- Nextcloud's server keeps the
- Switched in Settings ▸ Your folder and sharing: "Also send this device's changes to cloud.example.org directly", on by default wherever the backup is on (
remote.toml:send, never shared), with where it stands ("Sent directly to cloud.example.org, last at 10:41.", or why not). Off, nothing is sent; with the backup off, neither. - "Send everything again", a button in the same place, sharing on: each of this device's files compared with the server's copy, whatever was sent before, and sent where it is missing there or not the same (its folder failing to list,
blobs/with thousands of names on a slow line: each of its files asked of the server itself,Depth: 0: review of 18 October 2026, L-7); then the others' newer files fetched, and an exchange. It says what it did: "Sent 14 files to cloud.example.org at 10:42.", "Nothing needed sending: cloud.example.org held this device's 14 files already, checked at 10:42.", or why not (the folder not found on a server, sending switched off, the server's error). Sioul keeping the folder itself, the same: what it remembers of sending its files is forgotten (its sealed files apart, which tell its own from the others'), and the server's folders looked through again (remote::again_mirror). - Where: what was sent, when, and why not, in
$XDG_STATE_HOME/sioul/share/sending.toml, apart from the backup'sremote.toml, so that a pull and a send never write over each other's; never shared, never a password. One send at a time on a device, whichever process runs it (a lock beside the memory). Said in a phone's log ("backup: 10:42:03 sent to murena.io: 2 sent, 0 there already, 0 records longer there left as they are, 1840 bytes", and, after a listing, what differed there by kind and size: "records 70708 here, 71234 there"), never a name or a content. - Its entry put back there: a pull lists this device's own entry with the others' (
State::own_entry); listed after the last send went through and of another size than it was sent, it is an older copy put back there (another device's sync app, the server's own versions): the next send looks at it again and sends the entry over it, rather than at the check of every six hours (remote::tests::the_phones_entry_put_back_older_on_the_server_is_sent_again). The same bytes sent by a sync app keep their size, and cost nothing. - Taken out there: this device's records past their time (removed here once every device read past them), never its last round, and nothing while none of its records is here; the sealed files it swept; each over what is there as it was sent (
If-Match). eDrive sends again each file Sioul sent, the same bytes, and Nextcloud gives the file a new ETag: the delete is then refused (412). Until 15 October 2026 that file was forgotten here and stayed on the server for good (a phone's past full rounds, 47 MB on the owner's). Now the server's copy is looked at (PROPFIND): still of the size this device sent (records only grow, and only this device writes them; a sealed file's name is its content's), and for a sealed file still dated as this device sent it (X-OC-Mtime, the server'sgetlastmodified), it is taken out over its ETag there now. A sealed file dated otherwise was sent there again by a device that uses that content again (it made the file fresh): its use, never taken out; a round of another size is left to the listing; the look failing (a server's error, a timeout), the file is left there and the send goes on, not held back (review of 15 October 2026, L1:remote::tests::a_failed_look_after_a_refused_delete_holds_nothing_back). Any other answer to the delete (423while a sync app sends that very file again, a server's error,403) does the same; until the final review of 15 October 2026 (RES-1) it failed the whole send and held the next sends back, the records with them. A round left there is forgotten here and looked for at the next listing; a sealed file left there, which no listing looks for, is kept here and tried again at the next listing, never at each send (Sending::refused:a_delete_answered_otherwise_holds_nothing_back). A round gone here goes there only below the readers' floor (share::rounds_past, asremove_old_roundsjudged it): one gone from a folder synced again in part, or put back from a backup, stays there for a reader that has not read it (L2:a_round_gone_here_goes_there_only_below_the_readers_floor). A listing (every six hours, and with "Send everything again") takes out this device's rounds still there below the round its memory and the others' notes allow (share::rounds_past: below its last full round, and read past by every device heard of within 180 days; a device that read less holds the rest back): those the sync app left there, or that were forgotten before 15 October 2026. Only names of this device's own records; never another device's file; nothing while none of its records is here. Said in a phone's log ("… 2 taken out …"). The same listing takes out what this device took out here of the devices that no longer count (remote::Departed, from its memory:share::departed_past,share::swept_theirs): their past rounds still listed and gone here, over the ETag listed, and the sealed files it swept as theirs, each looked at first and taken out only while its time there is two weeks at least before it was swept here (another device that uses that content again makes it fresh, and its sync app sends the new time: that copy stays), over its ETag there now. Any other answer (404: another device took it out first;412: a sync app sent it again), or a failure, leaves it for the next listing, the send going on (a_dead_phone_s_past_rounds_go_from_the_server_too). So do the files of a device whose files you removed, while still as they were then (below, "Removing a device's files"). - Costs: this device's current records' file when it changes (32 KiB at most), its entry and its claims each minute while Sioul runs (some hundred bytes each), now and then a full round (a megabyte or two, once per megabyte appended). Measured on the stand-in server for a computer holding 8,000 entries and writing each minute, its claim renewed each minute: about 520 KB an hour (
bytes_an_hour_of_a_computer_writing_each_minute); the same computer before 8 October 2026 sent its 2.5 MB file each minute, 150 MB an hour, had it gone. With eDrive, the phone's own files go up twice (Sioul's, then eDrive's same bytes). - Tested (
cargo test --release -p sioul-sync remote), against the stand-in server, which now gives a new ETag at eachPUT, keepsOC-Checksumand honoursX-OC-Mtime, and two sync apps simulated as their sources read: only this device's files sent, never twice when the sync app sent them before or after, records never sent shorter, switched off not a request (beside_a_sync_app_only_this_devices_files_go_up_and_never_twice); sealed files whole and only this device's, taken out once swept (sealed_files_go_up_whole_and_only_this_devices); the server losing this device's files, sent again and said, a second time nothing sent, a server's error said (send_everything_again_after_the_server_lost_this_devices_files); doses never known wrongly, every dose arriving and the records there never shorter, with eDrive sending too and looking at the worst moments (it brought an older copy down over the phone's newer one, refused its own uploads:doses_are_never_known_wrongly_with_edrive_sending_too), and with the Nextcloud client on the computer sending too (conflicted copies made there, rounds started again:doses_are_never_known_wrongly_with_the_nextcloud_client_sending_too). On a throttled uplink (the stand-in server reading slowly): a large records file cut at a flat budget, said in detail and held, not tried before its backoff, then given the time its size asks and going whole (a_large_file_goes_through_a_slow_uplink); the server failing, nothing tried before the backoff, the count starting again once it answers (a_failed_send_waits_for_its_backoff); a text written while a large records file crawls up reaching the phone's pull within seconds (a_text_to_send_reaches_the_phone_while_the_records_crawl); the bytes an hour (above). What eDrive sent again taken out once past: a past round and a swept sealed file, each refused once then taken out over its new ETag, a sealed file another device sent again with a new time and a round grown there left, nothing asked again (what_edrive_sent_again_is_taken_out_once_past); past rounds left there taken out at the next listing, every six hours, a device that read less holding the rest back, never another device's file, nothing while none of its records is here (past_rounds_left_on_the_server_go_at_the_next_listing); six weeks of edits on a desk and a phone under eDrive, what the server holds settling (below, "What the folder holds over weeks").
Kept in step by Sioul itself¶
With a Nextcloud (Murena's, ownCloud's) that one of your contacts-and-calendars accounts reaches, no sync app is needed: Sioul keeps its own copy of a folder of your files there and sends and fetches the files itself, over WebDAV. Chosen when sharing starts (Settings ▸ Your folder and sharing: "Sioul keeps it in step with your Nextcloud itself", the account, the folder's place there), or reached through the server's own sign-in, with no account at all (below, "Joining through your Nextcloud's sign-in"): the folder is then kept with the sign-in's own login and app password (remote::State::user), whatever account the contacts and calendars use. The folder on the server is the same as a sync app's: a device with a sync app on it shares with one without, each in its own way. Code: crates/sioul-sync/src/remote.rs (step, open, create, begin), the window's side in crates/sioul-app/src/share.rs (start_on_server, mirror_step).
- Its copy:
$XDG_STATE_HOME/sioul/share/mirror/, the sharing folder as the exchange knows it (here.tomlnames it); what was fetched and sent inshare/remote.toml(mode = "mirror"), never shared. - Starting: the folder looked for in the account's files (
Documents/Sioulby default). Sealed already (another device shares through it): itsseal.tomlcomes into the copy, and the passphrase is the one chosen there (checked against it). Not there: each missing folder is made (MKCOL), the seal made here and sent only if none is there yet (If-None-Match: *): two devices starting at once never seal it twice ("Another device started sharing in that folder at the same moment"). - A step: the server's folder looked through as the backup does (
PROPFIND, a folder whose ETag did not change skipped for ten minutes), the other devices' newer files brought into the copy (records from where the copy ends, the rest whole); then this device's own files sent: those changed here since they went up (size and time), or changed or gone there (their ETag in the listing): its records first, then its notes to the others, its claims, its entry (it says how far the records went), its sealed files last. So no large sealed file on a slow line holds back the records, the doses' answers among them (7 October 2026: before, a sealed file failing to go up stopped the step before the records, and one slow to come down left no time for them in the step's thirty seconds). The others' sealed files come after the steps, never in one (below). A record naming a sealed file not there yet is read as a sync app's late copy is: the others wait for it. - The others' sealed files (
remote::sealed_step; review of 17 October 2026, M4 and H1): after the steps and never in one, on a thread of their own: the window's after its exchange's sending (another exchange at once writes those that came), a phone's background step beside its mail; never a dose's alarm, which waits for its steps. A phone's background exchange reads no notes nor papers (they wait for the window), but in this mode it asks for the contents their changes name all the same, nothing written: the background's sealed step brings them, and the window writes them as soon as it opens, offline too (review of 18 October 2026, L-2; before, only a window's exchange asked, and a phone opened offline lacked the others' new notes). A step waits a moment for it at most: the step's lock is taken only to note a listing and to put the files that came in their places, some at a time, the state written first and once for them (place_fetched), so that a step never takes a file fetched for one of this device's own. Firstblobs/: listed only once its ETag, as the steps' look through the folder saw it, changed, an hour at least after the last listing, or six hours on whatever; not again while a failed listing waits its backoff (an hour at least); its answer read a response at a time, never whole in memory (up to 64 MiB of names; Nextcloud says some 480 bytes a name), and never part of it taken for the whole: an answer that ends early, or leaves out the folder's own response, is no listing (end tags may carry spaces,</d:response >, as XML allows). What it tells: this device's own sealed files gone there go up again at the next push; the others' its copy fetched that the server no longer lists (their writer swept them) are let go, but those this device still needs (share::needed_sealed, said by each exchange infiles.json): the contents of the current notes and papers of the parts shared here, those it holds after deleting them many at once among them (a listing that leaves files out, a server restored from an older backup, never takes what a record names: review of 18 October 2026, L-1), those its changes waiting name, the contents replaced within thirteen days (a version kept here by reference may name one), its own; none at all while its memory does not say what it needs (an older Sioul's, until this one's first exchange). Then those it waits for (blobs::Wanted: the notes and papers whose records came, a computer's texts' media), by their names (GET blobs/<name>), the smallest first, 64 MiB a step at most (the first whatever its size), each kept only once it opens whole to its content (blobs::check); on a metered or slow connection, one of a megabyte or more at most every ten minutes, as the sending paces them. One not there, cut or damaged is asked again within five minutes in its first hour, then after its backoff (up to an hour); a text's medium asked for three days in vain is asked no more until the texts' page shows it again (review of 18 October 2026, L-8). Said in a phone's log, counts only ("sharing: sealed files: 2 fetched (14350 bytes), 0 did not open whole, 0 left for later on this connection; listed 5120 (2457600 bytes read); let go of 40 (2800000 bytes)"); the bytes are those read, the server's compression undone, so a night's mobile data can be less. Until 17 October 2026 a step listedblobs/every ten minutes, megabytes of names each time on a phone, fetched every sealed file there, and the copy kept them all for good. - A note put back here after its content left the server: its content, in this device's copy only as fetched, becomes this device's own once its change names it, and goes up only where the server no longer has it (one look first,
Depth: 0; thenIf-None-Match: *): the others write it (remote::tests::a_note_put_back_in_the_mirror_reaches_the_others_once_its_content_left,a_note_put_back_in_the_mirror_before_its_listing_reaches_the_others,a_content_fetched_and_named_here_is_sent_only_where_the_server_lacks_it). Until 17 October 2026 it never went up: the others waited for it, then said it missing for good (review of 17 October 2026, H1). - Never another device's file: each device sends only its own (
<id>-<n>.jsonl,<id>.toml,devices/<id>.device,leases/<part>/<id>.lease, the sealed files it made). Each goes over what is there as it was seen:If-Matchits ETag, orIf-None-Match: *when none was there. Refused (412: changed there meanwhile), sent again over what is there now, once looked at: it is this device's own. A sealed file goes only where none is (one there is the same: its name is its content's). Refused again, a server's error, a timeout: the step stops, said; nothing here is lost, and it all goes at the next step. - Taken out there: this device's records past their time (removed here once every device read past them,
remove_old_rounds), never its last round, and nothing while none of its records is here: those it remembers sending, and, whatever it remembers, those below the readers' floor (share::rounds_past), listed in that step and gone here; the sealed files it swept (blobs::sweep), by their ETag. A round of its own below that floor is never brought back into its copy (restore). Until 17 October 2026 a past round it no longer remembered sending (a delete refused, "Send everything again", its state lost) stayed on the server, and the next step brought it back into its copy: as on the owner's new phone, 17 October 2026 (remote::tests::own_past_rounds_left_on_the_server_go_in_the_mirror_too). And what it took out of its copy of the devices that no longer count (above, "The log", "Old contents go"): their past rounds listed in the step, over the ETag listed, never fetched again meanwhile; the sealed files it swept as theirs, every six hours at most, only while still as old there, never fetched again either. A device that still counts them lets them go from its copy once the server no longer has them (a_dead_phone_s_past_rounds_go_from_the_server_in_the_mirror_too). And the files of a device whose files you removed (below, "Removing a device's files"), the same way (a_removed_device_s_files_go_from_the_server_in_the_mirror). - Its copy lost (none of its records here): its own files come back from the server first, so that it goes on after its last record rather than numbering them again over what the others read.
- When: before each exchange, the server looked through (at most every twenty seconds, unless news is asked for: a dose's alarm, a waking); after it, what it wrote sent at once, its entry with it; where the sync app would be asked to look (a dose marked from a notification, Sioul back on a phone's screen), what changed sent; at the window's close (a computer waits five seconds for it) and when sharing stops, a last sending. A phone in the background: at each of its steps, in place of the sync app's twenty seconds. One step at a time on a device, whichever process runs it (a lock beside the memory).
- Said in Settings ▸ Your folder and sharing: "Kept in step with cloud.example.org by Sioul itself: Documents/Sioul.", when last in step, or why not.
- Costs: a step with nothing new: one listing (a few kilobytes), not said in a phone's log;
blobs/at most hourly, once it changed (above); a step's listings said in a phone's log when they read 64 KiB or more, or when the step fetched or sent something ("listed 12 (5130 bytes read)"). This device's records go whole when they changed (up to a megabyte before a new round starts): on a phone, which writes few, little; on a computer that writes each minute, more, on its own connection. - Tested (
cargo test --release -p sioul-sync remote), two devices with no sync app at all, through the stand-in server alone: do-not-disturb and a dose's answer both ways, each device knowing how the other is; a device beside its sync app and one without sharing one folder, both ways; a device never writing another's file; a refused (412), failed (500) or cut upload losing nothing and going at the next step; its copy lost, a device going on after its last record; sealed files travelling whole, never sent back, taken out once swept, let go on the other device once the server no longer has them; a sealed file failing or slow, going up or coming down, never holding back the records, and coming after its backoff (a_slow_or_failing_sealed_file_never_holds_back_the_records);blobs/listed seldom whatever its size, never in a step, its failure failing nothing, what is waited for coming by name and whole (blobs_are_listed_seldom_whatever_their_number_and_come_by_name), a listing cut short no listing (a_listing_cut_short_is_no_listing); a note put back after its content left the server reaching the others; nothing sent until it is set up.
Joining through your Nextcloud's sign-in¶
A device with nothing set up joins the others through its Nextcloud alone (16 October 2026: the owner's new phone, on which Murena's eDrive did not bring the Documents folder): no sync app, no account added first, no password typed into Sioul. Offered on the Porch of a device that does not share yet, as its first card, "Join your other devices" (Sign in to Murena, Sign in to another Nextcloud, I use a sync app's folder; Leave as is puts it away for good, porch-join-left), and in Settings ▸ Your folder and sharing above the two ways, on a phone and on a computer alike. Code: crates/sioul-sync/src/loginflow.rs (the sign-in), remote.rs (seek, begin_signed_in), crates/sioul-app/src/join.rs and qml/JoinPanel.qml (the window).
- The sign-in (Nextcloud's Login Flow v2):
POST <server>/index.php/login/v2, signed in as nobody; the server answers the page to open and where to ask for the result (an address and a token). The page opens in the system's browser (open_url): you sign in there as on the server's site, its second step included, and grant access to "Sioul (Android)" (the system's name, as the server lists the app password it makes among its devices and sessions). Sioul asks the poll address every three seconds, the token in the request's body, until it answers the server, your login name and an app password: Cancel, twenty minutes (the server's own limit), or the answer stops it. The network down a moment is asked again; half a minute in a row, it is said. Murena's server isloginflow::MURENA,https://murena.io: its sign-in answers there itself (16 October 2026: the page and the poll address onhttps://murena.io, withoutindex.php). Another Nextcloud by its address as typed (loginflow::address_of: "cloud.example.org" →https://cloud.example.org; its install's path kept, its pages after it left out; plain HTTP refused). - Kept to the server you chose: HTTPS only, never redirected; the page, the poll address and the server the answer names must each be on the origin chosen (scheme, host, port): one elsewhere is refused ("murena.io named another server, … : refused, nothing was kept"), and an app password come with such an answer is given back to the server that made it. The answer's
servermay carry the install's path (https://cloud.example.org/nextcloud): its origin is what is compared, its path what is used. - Secret: the poll token and the app password are never written to a file or a log, never shown by
Debug(no_debug_shows_a_secret), and go to that origin only; the sign-in's own copies are zeroed when dropped (loginflow::Secret,Grant), while those a request needs (its login, itsAuthorizationheader, a keyring read) are plain strings for that request, as every account's password is. The app password goes to the keyring at once, under a name of its own, "Nextcloud app password:on " ( loginflow::keep), for the sharing; and under the contacts-and-calendars account's own entry ("on ") when an account is made from it, or when yours on that server (its address's origin) has no password here and the app password opens it there ( loginflow::opens). Everywhere else it goes is a request'sAuthorizationto that server. Cancel after the sign-in gives it back (DELETE /ocs/v2.php/core/apppassword,loginflow::revoke) and forgets it here; so does Stop sharing, unless one of your accounts here keeps the same (join::let_go). Nextcloud makes the app password when you grant access on its page, not when Sioul asks for it: cancelled before Sioul received it, or Sioul stopped meanwhile, one unused "Sioul (Android)" app password stays among your Nextcloud's devices and sessions, which nobody can read once the sign-in expired; revoke it there. - The folder found (
remote::seek): in the account's files (…/remote.php/dav/files/<user>/, the user as the server's principal names it), a seal of Sioul's (share::is_seal) looked for atDocuments/SioulandSioulfirst, before any listing, then in each folder one level underDocuments(the first 40 by name, hidden ones left out): onlyPROPFINDandGETofseal.tomlfiles. A listing ofDocumentsthat fails only ends those extra places; a seal that cannot be read leaves the others looked at; the look's time running out (thirty seconds for all of it) ends it with what it found (the review of the join, M3: on a slow server,Documents/Sioulfound first was lost with the rest). Found: proposed ("Your other devices share through Documents/Sioul there"; several: one to choose), or another place typed, the passphrase typed once. None found, every place looked at: a new sharing may start there, the passphrase typed twice, as on a first device. The look incomplete or not possible (Sought::complete): "Sioul could not look through all your files on murena.io", with Look again, or a place typed and joined; never "none found", never a new sharing offered where the folder may be. A place typed where no seal is, from there, is said ("No folder shared by Sioul is at …"), nothing made. - Joined: the passphrase typed once, checked against the seal (
share::key_for: a wrong one refused, "This is not the passphrase chosen on your other device", the sign-in kept for another try); Sioul keeps its own copy of the folder in step with the sign-in's login (remote::begin_signed_in, as "Kept in step by Sioul itself", above); a first exchange runs at once on the join's thread (share::exchange_at_once: the server looked through, the others' files read, this device's sent), and the shared settings, accounts and parts arrive as a sync app's folder would bring them. A sharing started there instead (share::Begun::Started) is said as such, "Sharing started on murena.io, in Documents/Sioul. On your other devices, join it with the same passphrase.", never as joined, nothing said to wait for. - The contacts and calendars (
join::Agenda), once an exchange brought everything your other devices share: a whole look through the server gone through since the join (every device's records, entries and claims brought,State::last), no error said since, nothing left there too large to bring, and an exchange that read them (join::state_complete). A first exchange that did not (its thirty seconds over among large records, a server's error) decides nothing: "Your contacts and calendars on murena.io come once your other devices' settings have all arrived.", and a later exchange of the window decides them (share/agenda-pending.toml: the server and the login, never a secret;join::agenda_later, the app password read from the keyring; said in the status line). Decided early, the desk's Murena account not arrived yet, a second would be made beside it and meet it on every device (the review of the join, M2). Then one of yours whose address is on the sign-in's server (its origin: scheme, host and port, never its host's name alone, which Accounts keeps without its port, nor an account naming the server's host but pointing elsewhere: L2) with that login (case aside), or naming the same user in its address (…/calendars/<user>/), come with your settings or here already, is used: given the homes found there when it has none here, so that its first sync never looks for them elsewhere with the app password (L1), and the app password when it has none here and that password opens it there (an account whose login is the user name, where the sign-in gave an address, may not open: it then asks for its password once, as any account from another device; one with its own password here keeps it); never a second account for the same server and login. None: one is made from the sign-in (murena-agenda, as Accounts names one), its homes found on that server alone (dav::discover_on: itsremote.php/dav/, then its well-known addresses, every request and redirect on that origin, whatever the server or the login's domain names), and synced. A new sharing started there decides at once: no other device has settings to bring. - Said (
join::Stand): who this device shares with now ("This device now shares with desk and laptop."), that the settings came, the accounts, the hours (or that none are set there), the contacts and calendars ("use this sign-in" only where the app password was kept for them: L4); then, apart, what still needs a sign-in on this device: each account whose password does not travel and is not here (mail, mostly), and where to give it. - Add a device, on a computer that shares (Settings ▸ Your folder and sharing, a press): what the new device needs, in a few lines: where the join is on it; the server to sign in to (Murena's, or another Nextcloud's address) and the folder's place there, when the folder is known on a Nextcloud (kept in step by Sioul itself, or found there by the backup); else the same folder, carried by a sync app; then the passphrase, the one you chose, never shown (
join::add_device). - A phone in another's place: a phone that shares while another phone does is asked once, "Does this phone replace GS290, silent since Sat 10 Oct 21:40?" (
join::replacing; the one heard of last, and only once it has said nothing for a day as this device sees it: the later of when this device saw its entry change and its own last word, as a computer's "Forget it now" judges it, never that phone's clock alone (the review of the join, M1: a phone in use goes silent for hours with no network, switched off, or deferred by Android; on a phone just joined, the question comes a day on, the computer's "Forget it now" meanwhile); never one asked about, replaced or forgotten here;share/replacing.tomlkeeps whom it asked, never shared). Its help says what a Yes about a phone still in use does: it would leave your doses without that phone's word, and both could remind you of the same dose. Yes: its entry says it (replaces, below, "Devices"), forgotten on this phone at once, an exchange telling the others; No: a second phone, nothing changes. A device on Sioul 0.0.6 or older passes overreplaces: update the computer too, or the old phone keeps holding the doses there (until its "Forget it now"). - Tested (
cargo test --release -p sioul-sync loginflow,remote::tests, against the stand-in Nextcloud, which now has the sign-in, its app passwords, their revocation and its principal's homes): the sign-in answered once, its app password for its server alone, its token never in an address (a_sign_in_answered_gives_an_app_password_once_for_its_server_alone); cancelled, or left past its limit, nothing kept, nothing made (a_sign_in_cancelled_or_left_twenty_minutes_keeps_nothing); an answer or a poll address naming another server refused, the app password given back, nothing asked of the other (a_server_named_elsewhere_is_refused_and_nothing_kept); a principal on another server, or a login of another domain, never asked with the app password (the_app_password_never_leaves_its_server); the seal found atDocuments/Siouland elsewhere, a file that is no seal never taken for one (the_seal_is_found_at_documents_sioul_and_elsewhere); Sioul's own places read before any listing, a failing listing ofDocuments, the time out among the extra places and a seal that cannot be read each keeping what was found and saying the look incomplete (a_slow_or_failing_look_keeps_what_it_found_and_says_it_is_incomplete); a first look cut short among the desk's records never taken for a whole one, the next bringing the desk's account (a_first_look_cut_short_is_never_taken_for_a_whole_one); a fresh device, no sync app, no account, no configuration, joining through the sign-in, a wrong passphrase refused, the desk's settings and accounts arriving at its first exchange, the desk hearing of it, the app password in no file (a_fresh_device_joins_through_the_sign_in_and_receives_the_others_settings). In the window (dbus-run-session -- cargo test --release -p sioul-app join): the account made, or yours used, never a second, matched by its address's origin (the_account_is_made_from_the_sign_in_or_yours_is_used), decided only after a complete first exchange (the_contacts_and_calendars_wait_for_a_complete_first_exchange), a look that could not go through never offering a new sharing (a_look_that_could_not_go_through_never_offers_a_new_sharing), the question asked once, after a day as this device sees it (a_phone_is_asked_once_whether_it_replaces_another);tools/qml-test.sh tst_join.qml(each step of the panel, the sign-in's two ways in English and French, a sharing phone's question, a computer's Add a device and Forget it now); the window's pictures,tools/demo/run.sh --release OUT en join(andfr, andphone). Not tested: a real Murena sign-in (its page, its second step, the app password it makes for the WebDAV files).
What the folder holds over weeks¶
What the sharing folder holds, on its server too, settles at the current contents and records, and what the last two weeks replaced. On 14 October 2026 the owner's held 349 MB, 282 MB of it sealed files: the phone's past full rounds left on the server by eDrive (47 MB), contents replaced kept 90 days and dated anew at each rebuild, four earlier takes of the owner's recordings (15.7 MB). Changed on 15 October 2026: above, "Old contents go", "The log" and "Sent to the server too".
- Simulated (
cargo test --release -p sioul-sync six_weeks_of_edits -- --nocapture): a desk and a phone sharing through the stand-in Nextcloud for six weeks, the phone under eDrive (each file sent again, no deletion passed on) and sending its own files itself; 36 notes edited a day on the desk, a recording of 1.1 MB saved in three takes minutes apart every six days, a paper of 400 KB replaced every ten, a note of the phone's each day, the phone's texts an archive of 3,000 growing by 150 a day. Every content replaced leaves the server within two weeks and a day, whichever device sealed it; no device's records below its floor stay there; a recording's earlier takes never reach it. From day 14, the replaced contents still there stay between 2.8 and 4.3 MB (some 500 files: two weeks of edits; a recording replaced adds its 1.1 MB for a day or two) while those replaced since the start grow to 8.3 MB; the phone's records hold its last full round (1.7 MB, then 3.4 MB from day 21, the earlier rounds gone from the server that day) and the rounds continuing it; the server holds 8.6 to 10.8 MB from day 25 to day 41, growing only with the texts' archive. By the rules before 15 October 2026, every content replaced in those six weeks would still be there, and every past full round of the phone's that eDrive sent again. - A device that no longer counts (16 October 2026): the owner's folder held 431 MB, 99 MB of it records. A phone dead for good since that morning held the new phone's first rounds back (16.3 MB, restated by its full round), as its plain notes would for 180 days, and its own past full rounds (47.1 MB), restated by its last full one, were left on the server by eDrive: nothing ever took them out but itself. Once it no longer counts (forgotten on each device, or replaced by the new phone), about 63 MB leave the folder by themselves; its last full round and the rounds after it (16.3 MB) stay, holding its last word, its texts' archive with it.
- Removed notes and a dead phone's last rounds (17 October 2026): the owner's folder held 389 MiB: 336.7 MB of sealed files (132.3 MB of texts' media, about 204 MB of notes and papers, among them the contents of notes deleted in bulk ten days before, still waiting for "Take them out everywhere"), and the rounds of three devices (2.9 MB, 16.1 MB for the dead phone's last full round and the rounds after it, 32.7 MB for the new phone's, its rounds below its full round left on the server: above, "Kept in step by Sioul itself"). Removed notes now leave by themselves two weeks after they went, their contents as soon as every device read so; "Remove its files" takes out a dead device's last rounds (above, "Removing a device's files").
- What stays as large as it is: the current contents (the texts' media, the notes, the papers) and each device's records from its last full round on.
- The format (15 October 2026): no record and no file of the folder changed meaning. Each device's sealed entry gains
keeping, which Sioul 0.0.5 and older pass over; replaced contents leave the folder after two weeks only once every device says it keeps them so, and stay 90 days while one runs 0.0.5 or older, as that version counts on. Full rounds come less often, and every version reads them as before. Nothing of the folder changed on 16 October 2026 either: who counts, the departed devices' rounds and what was swept of theirs are kept in each device's own memory (memory.json,files.json), never shared; an older Sioul's memory says none of it, and is read by the rule as it was until its next exchange.
Removing a device's files¶
A device that left, that you forgot, or that another replaces keeps its last full round and the rounds after it in the folder, its last word and its texts' archive with them (above, "The log"): on 17 October 2026, a dead phone's 16.1 MB of the owner's. Remove its files, on its line in Settings ▸ Your folder and sharing (under Devices that no longer count; for a device that still counts, on its line, Forget it and remove its files), takes them out: what goes is planned outside the exchanges' lock (every entry read, each round and sealed name looked at, its small files hashed: review of part 3, J1), and the sharing's memory written as it will be outside it too, under hidden names, synced and closed (Memory::prepare_save); the lock is then taken only to see that no exchange wrote the memory meanwhile and to put its two files in place by renames, never more, however many files the device had or however large the memory (R-6: on CI's Windows job of 20 October 2026, writing the memory under the lock, the antivirus reading each file at its close, kept a dose waiting 0.79 s); an exchange having written it meanwhile, all of it is done again outside the lock, five times at most, then said busy (share-busy); then its files are taken out a fifth of a second at a time, its rounds oldest first, the lock left free a moment between two holds (share::in_short_holds); an exchange between two reads the memory written first, and takes what is left of its rounds and small files out itself. The window starts it once its own exchange running is done, without holding its exchanges back meanwhile (after_exchange): a dose's alarm waits a fifth of a second at most, and the file being taken out then, even for thousands of files (until 20 October 2026 its exchange waited for the whole removal; 156 files each taken out 10 ms slower: 0.20 s at most, removing_a_device_s_many_files_never_keeps_a_dose_waiting; a device of 1,502 sealed files, each of the memory's files written 600 ms slower as on Windows: a dose's exchange waiting 23 ms at most, 0.60 s with the memory written under the lock, removing_a_device_plans_outside_the_lock; planned again outside the lock each time an exchange wrote the memory, each plan 300 ms: 0.05 ms, removing_a_device_plans_again_outside_the_lock). Code: share::device_files, remove_device_files, departed_devices in crates/sioul-sync/src/share.rs; the window's side in crates/sioul-app/src/share.rs (device_files, remove_device_files, gone_rows) and qml/SharePanel.qml.
- Said before the press, in plain words (
share::DeviceFiles): what goes, with its size (its rounds and the small files it kept there; the sealed contents that go now, and how many more go later), and what stays (what each device already read of it, the texts' media another phone's log names, its entry and its note when they stay), and which devices that count have not read all of it yet: what they have not read never reaches them. A device heard from within the last day is said to be in use now, first (share-remove-in-use: when it was last heard from): pressed all the same, the sharing forgets it, but the doses keep their doubt for it until it says anything newer, never "taken" or "not taken" said in its place (review of 17 October 2026, L7). A device joining later gets none of its records, unless another device's records carry them (texts restored on a new phone). - What goes: every round of its (its last full round and the ones after it too), its claims (
leases/<part>/<id>.lease), its texts and replies to send (texts/send/<id>.jsonl,phone-messages/ask/<id>.jsonl), in the folder and in what was fetched beside it; its note of how far it read (<id>.toml) once it left or another device replaces it (forgotten here first, then replaced: replaced, both go at the next exchange), as no device counts it then (one you only forgot here may still count on your other devices, where that note says how far it read: without it, it would hold every round back there); its entry (devices/<id>.device) only once nothing needs it, another device's sealed entry saying it replaces it, or it left 180 days ago: before, its entry is what tells your other devices that it left, or how it closed, the doses first (a device whose entry no longer reads is known by its claims, as an older Sioul, and with none left the doses would doubt for it until it had been silent 180 days;share::small_go); and the sealed contents of its last words on notes and papers that no other record names (never one another device's current record names, a change waiting, one this device sealed, a medium a text names, nor another device's content replaced lately, which a device may keep by reference, nor a note's or a paper's held here after going many at once, which "Put them back" needs (review of 17 October 2026, M3); none while a part whose files are sealed apart is not shared here, or the texts' media are not known, as for the sweep). One of those sealed or used again lately (made fresh within the sweep's two weeks, 90 days while an older Sioul counts: a device may have sealed the same bytes in a record not read here yet) goes later, swept as a departed device's replaced contents are (Sealed::theirs); its replaced contents go that way too, in their time. - Each device keeps what it read: its notes, its papers, its settings and lists stay as they are on every device; nothing is taken out anywhere but its files in the folder.
- Only by sealed facts: refused for a device that counts here (
share-device-counts): the window forgets it first, for the doses too (health::device_forget, as Forget it now), and a device that no longer counted is forgotten by the doses too before its claims go (health::device_forget_doses): it stops counting for them exactly as a forgotten one. A hostile server or folder can never make it remove anything by itself: only your press does, and only the files of the device you named, never a record of another device's. - Never fetched again, taken out there (
Memory::removed_devices,share::RemovedDevice: when, its last round and that round's size, whether its entry and its note went): the rounds before its last stay out by the floor of the rounds past (Memory::departed); its last round while it is no longer than it was (a copy a sync app brings back), and each small file that went while it is as it was last seen there (its ETag,remote::Departed::removed_as_known; in the folder, its content when it went, by its hash: a claim it wrote since, of whatever size, stays): each exchange takes such a copy out of the folder again, and the sending and Sioul keeping the folder itself take them out of the server and never fetch them (remote::tests::a_removed_device_s_files_go_from_the_server_in_the_mirror). - A device still in use whose files you removed shares again by itself: under a sync app, its own records gone from its folder, it opens a full round restating its last words (
share-own-cut); Sioul keeping the folder itself, it goes on writing its last round, which then grows past its size and comes, with its claims renewed and its entry said again (its own copy keeps its earlier rounds and sends them up again: they stay out until it counts again here, a few megabytes for a device removed by mistake); once it says anything newer, it counts again, and is no longer removed (share::gc_tests::a_device_still_in_use_shares_again_after_its_files_were_removed). - Tested (
cargo test --release -p sioul-sync gc_tests): a computer that left (remove_its_files_for_a_device_that_left), one forgotten and one replaced (a_forgotten_device_s_entry_and_note_stay_a_replaced_one_s_go), one still in use, a text's medium (a_text_s_medium_stays_when_a_device_s_files_are_removed), the notes held after going many at once (remove_its_files_leaves_what_put_them_back_needs), one forgotten then replaced (forgotten_then_replaced_its_entry_and_note_go), one with many files beside a dose's exchange (removing_a_device_s_many_files_never_keeps_a_dose_waiting); on the server, in the mirror. In the window (tools/qml-test.sh tst_share.qml): the notes held and their two buttons, "Remove its files" and "Forget it and remove its files" saying what goes before the press, Cancel removing nothing; the sentences themselves (dbus-run-session -- cargo test --release -p sioul-app share:notes_held_are_said_in_words,what_remove_its_files_takes_out_is_said_first).
Knowing what the others wrote¶
For the doses (docs/health.md, "Knowing"), a device must know whether it has read everything another wrote, and whether that one may still hold an answer not sent. Its entry in the registry says its session and how far it wrote its records at its last export (below, "Devices", Entry::wrote); an older Sioul's claims say how far it wrote (round and number, lease::Claim::wrote), and whether it closed (closed: Sioul quit, or a phone put it away, after one last exchange). Each device keeps the round and number of the last record it read from each other (Memory::read_n, share::heard). A line that does not read is never passed over in silence: the problem is said (share-other-line), and when its computer wrote it, at the latest, is kept (Memory::broken, the latest such date kept): the clock of the line after it in its round, else now, never the line before it (days older, maybe: until 10 October 2026 it was, and a dose marked in a lost line could be known not taken), and a line met before (a round read again) is not stamped anew; a dose due up to twelve hours after it (health::BROKEN_SPAN: marked ahead of its time) or before is then in doubt. A lost line is never counted as read (read_n stays before it), so what its computer wrote is not all known here, the doses in doubt, until that computer's next full round (share::correctness_tests::a_lost_line_is_never_counted_read_and_a_full_round_mends_it). Until 10 October 2026 a years-old damaged line read again doubted every dose of the next hours (share::correctness_tests::a_lost_line_is_dated_by_its_own_time_and_stamped_once). Each other device is heard of by its notes, its entry, or its last record that opens, the latest (share::others_heard): a device whose records came before its notes and its entry counts for the doses, in doubt, until its entry comes (a_device_known_by_its_records_alone_is_heard_of). A file this device can no longer trust (the doses' record or their records, broken or lost) can be rebuilt (share::rebuild): its entries are forgotten here, so nothing of it is taken out elsewhere, and read again from every device's records, this one's too, at the next exchange. The doses' repair sets the record aside in the same step, under the exchanges' lock (share::rebuild_then): run apart, an exchange between the two filled the lost record again from the records, the full record then went aside, and the next mark written took every other mark out on every device (review of 15 October 2026, P1: share::correctness_tests::the_doses_repair_is_one_step_and_takes_no_mark_out). At that exchange a file rebuilt with its copy left here (share::rebuild) joins as a store new here does: it is read from the records first, then only what they lack goes out, so its copy, which may be what was there before, never goes out first with a clock later than all it read, over another device's newer word not read yet (a dose's mark the phone had taken back came back on both devices: a_rebuilt_record_never_sends_an_old_mark_again). A file set aside in the same step (share::rebuild_then, the doses' three files) holds only what was written after the repair, and that goes out as any change made here does, while the rest comes back from the records: a mark made after the repair is never replaced by an older record of the same dose, and the old marks went aside with the broken file, so none comes back (final review of 15 October 2026, RF-J: while such a file joined, a dose marked again after it was taken back elsewhere was lost on every device, a_dose_marked_after_the_repair_goes_out_over_an_older_take_back; the reminders' file alike, a_reminders_word_written_after_the_repair_goes_out; the_repair_sends_no_old_mark_again). The doses' record writes no chat minutes while it has none (chat_minutes is never written at 0), so a new record never resets the day's count on the other devices (it went back to nought on every device after a repair: after_the_doses_repair_every_mark_and_the_chat_minutes_stay); a minute of chat counted between the repair and the next exchange still starts the day's count again from that minute. Files the window writes too are written under a lock beside them (sioul_core::filelock, .<name>.lock, which hidden names keep out of the sharing). Every writer of the window applies its change as an operation (add, take out, change by id or by key), holding that lock, on the file read again once it is held, as the sharing and the switch do, never as a whole list or a whole file it read earlier: a change written in between is never lost. So:
- A setting from its row (
settings::change, the window'schangeSetting): the row sends the value it showed with the new one (one that does not read is refused, said); a list (words, days off, working hours, the pause's helps, the gathered reminders, the projects shown on the Porch, a project's routes, the senders' lists) is the row's change set over the list as the files hold it now (settings::rebase): what the row took out taken out, what it added added, the rest as it is now, the row's order kept. An element of a structured list the row changed (a day off, a day's hours, a route) takes the place of the one the list holds now (settings::rebased_by): a day off found by its id, whatever it holds now, so that changed on two devices at once it stays one, the later change; a day's window by its rank in the day only while the window there is the one shown, else by what it held wherever it is (a window added elsewhere that day shifts the ranks), and when it is gone, nothing is replaced and the other device's windows stay beside this change; a route (no id) at its place while it holds what was shown, else by what it held wherever it is, else at its place while the list holds as many as shown (the same route, changed elsewhere: the later change). A day off already there keeps its table and its id; a new one takes an id once the list carries ids. A pinned site's categories are still sent whole for that site (the site travels as one entry, the later change winning). A row of attention sets only the cells it changed (attention::apply_change); a task kind is added, renamed or taken out by its id on the kinds as they are now (config::change_kind); a folder kept or set aside, on the account's list as it is now. - The mail filters (
filters::save) and the meals, naps and night (Needs::rebased): the editor's change from what it showed, set over the file as it is now; a meal taken out moves the days' changes after it, on the days as they are then. - A bank rule is named by its id; one without (an older Sioul's) by its place, only while it still holds what the window showed, else "it changed meanwhile" is said (
accounts::split_at). - The stores kept whole (the contracts, the papers, the bank's movements, the letters, the medicines, the days' meals and naps:
change,change_file,try_change): read, changed and written under the file's lock, never across a long wait (a scan read, a task made, a file copied happen first, outside it). The sender lists are changed line by line under theirs. The letters' list travels as a note: the sharing writes a note under its lock when one of Sioul's own writers takes it (filelock::has_lock). - A whole state (the lighter days and pauses, the day's choices, the money's and the Porch's states) merges on saving (
filelock::save_merged): what it changed goes in, what changed under it since it was read stays; a list or table it emptied (a state skips an empty field) takes out what it held, never what another device added since; a file missing when it was read is read as empty; a field this Sioul does not know (a newer one's) is never taken out. The Porch's mark per account never goes back to an older message within one mailbox. An undo (closing the day, a pause) puts back, over the file as it is now, only the fields the act changed, and each only while it still holds what the act set (one another device changed since stays); the lighter days it added are taken out, another device's kept (Overrides::put_back). A session moved to another month is read and changed under its month's lock, written into the new month, then taken out of the old one.
Devices¶
Each device says how it is, in the folder, for the doses: <folder>/devices/<id>.device, written by that device only, sealed like the claims (bound to "device" and its id: an entry copied under another device's name does not open), its size changed at each writing (pad_for), never read past 64 KiB. Code: crates/sioul-sync/src/devices.rs (the entry, sealed, read, written), crates/sioul-app/src/devices.rs (the sessions).
- Who: the sharing's own id (
here.toml: a UUID made once per machine, never the host name, which can change, or be another machine's too), its name (a computer's host name; a phone's model, said "the phone" by the others), its kind (phone,computer), the Sioul that wrote it: its version and the commit it was built from (version,commit:sioul_core::build, building.md), the format of what travels that it reads (format, above, "The format of what travels"; none from an older Sioul), and how it keeps the sealed files no record points to any more (keeping: 1, two weeks; 2, the same, and a file another device took out kept as a copy among its earlier versions, from 17 October 2026; none from Sioul 0.0.5 and older, which reads the entry as before, the field unknown to it passed over; above, "Old contents go"). - The builds that wrote to the folder: each device's entry says its own, so the folder records which builds write to it. Settings ▸ Your folder and sharing says this device's ("This device: Sioul 0.0.3 (eff8661abcde).") and, under each other device, the one its entry says ("Sioul 0.0.3 (eff8661abcde)."), calmly when older than this one ("…, older than this device's (0.0.3 (…)): updating it there, when you can, keeps both alike."; versions compared number by number, two commits of one version never). An older Sioul's entry, with its version alone or neither, reads all the same (
devices::tests::an_entry_says_its_build_and_an_older_one_still_reads). - Its session:
started(its last start),closed(its last clean close; 0 never),working(up from the start to the last step of a clean close; required: an entry without it is no entry, never one read as closed). On a computer, a session is the window's life: up when Sioul starts; down as the last step of a clean close, after a final export: the window closed, or SIGTERM, SIGINT, SIGHUP, which quit Qt as the window's close does (cpp/signals.cpp), both ending inshare::closing. On a phone, a session is the time on the screen: down when Sioul goes to the background, after its export (Android may end a process in the background without a word), up when it comes back. A reminder handled while no session is open (a dose's alarm, "Taken" pressed on its notification) is a session of its own: up, what it records, its export, down, then this device's files sent to the server (beside the sync app, Sioul's own send) and the sync app asked to look unless Sioul's own pull works, so that the lowered entry goes up, not the raised one. Only when the last session of the process ends does it go down, and only once an export read everything that process recorded of the doses since (a mark, a record, a reminder:devices::recorded): a final export that failed leaves it up, and the others in doubt, rather than sure of what they lack. A crash leaves it up; a session that finds it up owes an export before its own close. - Its sharing:
imported(when it last read the others),exported(when its files were last read for an export: everything captured there before is in its records, up towrote, its round and number then),doses(it shares the health part),notifications(a phone: whether Android lets Sioul hold other apps' notifications there, as it last looked at each writing; unsaid on a computer and from an older Sioul: a computer's What reaches you counts a phone's messages unless it says no, an unsaid one as holding them),active(a computer in use: whether a person is at it, as it last looked at each writing: input within ten minutes and its session unlocked, as its system says (sioul_sync::power::present_of: KDE's and GNOME's screen savers and idle monitors, logind's hints; Windows' last input and its session's lock; macOS's last input and its screen's lock), or a press in Sioul's window or of do-not-disturb's switch less than ten minutes ago, which counts at once; unsaid on a phone, on a computer whose system says nothing of it, and from an older Sioul),left(it stopped sharing: it counts no more). A phone's background steps readactivealone, and only while you sleep (devices::others_in_use: then a computer in use counts while someone is at it, or while it says nothing of it, Sioul running then counting as before; awake, it counts while its Sioul runs); the doses never read it (Entry::said). Its value is read from the session as last read on a thread of its own (power::watch,kept_session): writing the entry never waits on the system's bus. An older Sioul reads the entry as before, the field unknown to it passed over, and keeps its own rule: no field changed meaning, so the format stays (devices::tests::an_older_device_reads_the_entry_that_says_whether_someone_is_at_it). - What it replaces (
replaces): the devices this one replaces, as you said when it joined (a phone that will never share again: dead, lost; above, "Joining through your Nextcloud's sign-in"), each with what it had last said of itself then, by its own clock: its entry's last start, and the latest of its entry's times, its claims' renewals and its records' clocks read (devices::Replaced). Every device then counts it as forgotten (share::present: it holds no form of what travels back, no part, no sealed file kept longer;health::apply_replaced: no word owed on a computer's opening, no dose waited for from it) while it says nothing newer than that: a new start, a later time, a claim renewed since, a record read past it. A phone said replaced by mistake counts again by itself the moment it speaks. Read from the entries as they read now and from the newest copy of each kept, so that an older copy put back never takes the replacement away. Nothing it said is lost: its last full round and the rounds after it, its texts' log and their media stay (texts.md); its older rounds, which that full round restates, and its contents since replaced, which no record names any more, leave the folder, taken out by the devices that count (above, "The log" and "Old contents go"). Sealed with the entry; an older Sioul passes over it. - A phone's word (
acked): on a phone only, the opening (started) of each computer's entry it read, of each computer it counts now (said off, forgotten, silent a week: gone from it), the latest it ever read, never lower (an older copy of a computer's entry put back does not lower it). Sealed with the entry. A computer reads it before it reminds a dose plainly as its first: every phone in the folder not stopped sharing and not forgotten there must name its current opening, that very one, and have been heard there within two hours (health.md, "What became of a reminder"). - Written at each start and clean close, and after each exchange, whoever runs it (the window, a dose's alarm, a phone's background service), an exchange never touching the session. Kept privately in
share/device.toml(never shared), changed under a lock and read just before: two processes (the window's, a background service's) never write an older session back over the other's. Into the folder only when the others need it (devices::change): at each start and clean close; at each export while the device is in use (working: the doses need its exports fresh,sioul_core::health::FRESH), whether someone is at it or not: one cadence, so that the server, which sees when the file changes, cannot tell when someone sits at a computer; whether someone is travels sealed with the rest, the file's size padded as at every writing (devices::tests::a_computer_writes_its_entry_at_one_cadence_whoever_is_at_it; for some hours of 13 October 2026 a computer with nobody at it wrote it every three minutes only, which told its presence by the cadence); otherwise when anything else it says changes (how far it wrote, its doses, its build, its format), when the folder's copy is not the one it wrote (a sync app putting back an older copy: its size and time differ), and once an hour (devices::RESTATED). A device put away whose exchanges find nothing new writes nothing there: the doses never miss it, a device closed after its last export being known by its close and how far it wrote, both written at once (devices::tests::a_device_put_away_writes_its_entry_only_when_it_says_something_new,the_phone_put_away_and_quiet_is_known_without_rewriting_its_entry). What it last wrote there is kept beside, never shared (share/device-published.toml: what it said but for its exchanges' times, as a hash, the file's size and time, the folder, a mark of the key it was sealed with, when): the folder sealed again (another passphrase, the same place), the entry is written under the new key at once. - Read by
health::know: each device's newest entry, by its own times (devices::written: an older copy put back is no news of it and never replaces it, the ninth review of battery part C's W1), when it came (on this device's clock), how far its clock is ahead at least (its export's date less this device's clock when it came: a lower bound, the largest of the last sixty kept), and whether everything it wrote up to its export is read here (Heard::complete). An entry that reads more than ten minutes ahead of this device's clock when it is read is set aside while it reads so (a late copy, within ten minutes by then, is taken): no news, no word of a phone, nothing it says counted, its doubt and Settings saying its clock runs ahead (F1; the final review before 0.0.6, F1H). A phone's word stands only where the copy read now says it as well as the newest one: a copy that is no news may take a word away, never give one (F1H). An entry that no longer reads is none: that device is known by its claims, as an older Sioul is, never as it last said. So is an entry saying it closed while its claim, renewed open more than a minute after that close (both on its own clock), says it runs: an older Sioul put back there, which updates no entry. Kept here:share/peers.json(never shared), with what you said: a device off, or forgotten, until it says anything newer (Forget this device, once a device is silent a week; on a computer, a phone silent for a day or more is offered Forget it now at once, with what it changes: it no longer holds the doses' reminders back, and counts again if it ever shares again;health::FORGET_NOW_AFTER); written whole beside it, then renamed over it, so that a crash in mid-write never loses it (until the final review of 15 October 2026 it was written in place). - What it costs: one small file per device, rewritten at each exchange while the device is in use (a minute while Sioul runs on a computer, whoever is at it), as the claims are; a phone put away, about once an hour, or when what it says changes. Until 10 October 2026 it was rewritten at each exchange, a phone's background step's too: some 500 writes a day into the shared folder, each sent by the sync app and by Sioul. While you sleep with nobody at a computer, a phone's background steps no longer come every two minutes (android.md); its pulls, every fifteen minutes then, find the folder changed all the same (the entry each minute, the notes to the others every fifteen while in use:
<id>.toml), so each pull reads the devices' folder too.
Leases: one computer at a time¶
Some work must happen on one computer only: reminding a dose (twice, it could be taken twice), numbering invoices (twice, one number twice), the gathered notification of sites. sioul_sync::lease keeps them apart through the same folder: each computer writes its own claim on a part, sealed like the records (<folder>/leases/<part>/<computer>.lease, one writer per file), renewed each minute while Sioul runs, alive five minutes after. Every computer reads the claims the same way:
- following you (medicines, the gathered notification): the live claim of the computer used most recently keeps it;
- staying put (invoices): the claim taken on purpose last ("Make invoices on this device"), else the oldest.
A computer acts only once it has kept a part for a minute and a half, time for the others' claims to come through the sync. Invoices also want the other computers holding them heard from within three minutes: a stopped sync could hide a claim, so they wait rather than risk a number twice. For invoices a claim that lapsed without saying it closed still holds them, a day after its last renewal (
lease::UNCLOSED): a computer whose sync stopped, or whose claims a phone's sync app brings half an hour late, may still be numbering. Until 10 October 2026 such a claim counted for nothing after its five minutes, and the phone could number invoices while the desktop did too (lease::tests::invoices_stay_with_a_computer_not_heard_from_until_it_closes). A computer gone without closing (a crash, a computer given away) holds them a day, then no more. Sharing off, this computer keeps everything.
Do-not-disturb's switch and list¶
Do-not-disturb on every device (do-not-disturb.md) adds two files, and needs no merge of the sharing's own:
- The switch (
state/do-not-disturb.toml, settings): a table per device, written by that device alone and shared whole (whole = ["device.*"]), its last press stamped with a hybrid clock (never before a press already known there). Every device reads every table the same way: the latest press wins; two presses that did not know of each other, less than a minute apart, "on" wins. Each table also says what holds on its device and whether its system is silenced, for the others' status line. - Who may reach you during do-not-disturb (
config/dnd-people.toml, senders): one entry per person ([[person]], by itsid), names, numbers and addresses kept in the sharing itself, since the address books of two devices may not be in step. - Neither is ever written over when it does not read; an older Sioul keeps their records waiting, unknown to it, writing neither and taking nothing out (
known_part).
Sealed¶
- Each record is encrypted on the computer with XChaCha20-Poly1305, bound to its computer, round, number and clock. The folder and its server see which computer wrote, when and how much, never what: not the entries' names (they hold addresses), not their values.
- The key is made from a passphrase with Argon2id (64 MiB, 3 passes, a salt in
<folder>/seal.toml), typed once on each computer (twice on the first) and kept in its keyring.seal.tomlalso holds a value sealed with the key: a wrong passphrase is told at once. At least 12 characters. - Lost, the passphrase cannot be found again: stop sharing everywhere, start again with a new folder; each device keeps its own files. Stop sharing asks first, saying what stops and what stays (
share-stop-ask). - Yours alone on the disk (Unix): what the sharing writes is 0600, the folders it makes 0700, as the calls' logs are (
share::private_dirs,new_private,make_private): the records, the sealed files and the seal in the folder; its memory, the copies before sharing, the versions it keeps; the files it brings in from your other devices (settings, lists, notes, papers) and the conflict copies beside them. A file there already that others could read becomes yours alone the next time the sharing writes it (written beside then renamed, it takes the new file's mode; appended to or copied, its mode is set); nothing sweeps your folders, and a folder there already keeps its mode. A sync app carrying the folder must then run as you, as the usual ones do (Nextcloud's client, Dropbox, Syncthing started by your session); one running under another account of the computer would not read the records. A phone's shared storage and Windows have no such modes (share::tests::what_the_sharing_writes_is_yours_alone).
Notes¶
- The notes travel with their folder's own sync, or, their part switched on, through the log, file by file (Notes and papers in the vault).
- A link keeps its file's name (
sioul:note/admin/lease.md, relative to the notes folder), so it holds on every computer, wherever the folder is. - A linked note not on this device yet (its sync not done, or the file removed elsewhere) shows faded, with its name and "Not on this device yet: it may still be syncing (Nextcloud, Dropbox)."
- A note renamed in Sioul updates its links (tasks' LINK lines,
links.toml); the other computer gets the new file from the folder, the new links from CalDAV and the log. - One writer at a time: the window and an agent (
sioul mcp) read a note again, change it and write it holding one lock, in Sioul's state folder and never in the notes folder (notes::with_lock,.notes.lock): a tie written into its front matter, a note saved, renamed, or made from a title (a free name found and taken together,notes::create).links.tomlis changed under its own lock (LocalLinks::change,.links.toml.lock), the one the sharing takes to write it, read again once held. Two writers at once lose nothing (links::tests::two_writers_at_once_lose_nothing, the MCP'swriters_at_once_lose_nothing).
Tested¶
cargo test -p sioul-sync devices: an entry sealed, read, padded, its build said and an older one without it read, refused under another name or without working; whether someone is at a computer, read by an older Sioul as before, and a computer that says nothing of it counted in use as before, a computer nobody is at counted only while you are awake (an_older_device_reads_the_entry_that_says_whether_someone_is_at_it, a_computer_counts_in_use_for_a_phones_rhythm_while_someone_is_at_it); a computer's entry at one cadence whoever is at it, the doses known from a phone's looks as before (a_computer_writes_its_entry_at_one_cadence_whoever_is_at_it); two processes never undoing each other's session; two devices and one folder: a phone marking "Taken" from its notification in the background and the desktop showing it taken, one raised but not exported, records behind the entry, answers that differ kept on both, a canary opened on both, an older Sioul taking nothing out. cargo test -p sioul-sync share: what the sync app must do (above: five kinds of carrier, eDrive's rule on each rewrite, a file put back shorter, a memory restored or lost, a gap, a damaged line, a half-written line, a big round, stores that leave, arrive or move, a settings file emptied), two computers joining (settings, accounts without their local paths, sender lists summed, a draft, a session), working apart (a session each in the same month, a setting each, a draft sent on one), the later word winning across two lists, a broken file left alone, the seal (wrong passphrase refused, nothing readable in the folder), rounds started again and old ones removed, a third computer joining from the latest round, a phone's notes folder getting projects, budgets and bank movements (the projects part) and both keeping what each added. Parts and notes: each device choosing its parts, never what tools make nor what another part carries (each_device_chooses_its_parts); a big folder sending only what changed, never reading again a file of the same size and time, a file over 64 MB staying (a_big_folder_sends_only_what_changed); a note changed on two devices keeping both, whichever came later, and whatever carries them, with a note taken out on one and changed on the other (a_note_changed_on_both_keeps_both, a_note_changed_on_both_keeps_both_whatever_carries_them); a part switched off taking nothing out and sending nothing, switched on again joining (a_part_switched_off_takes_nothing_out_and_sends_nothing); earlier versions kept and put back, a setting and a note, and a note taken out (earlier_versions_are_kept_and_put_back); a damaged sealed file, a placeholder, a record that is none and a line that does not open never erasing the file here (a_damaged_blob_or_record_never_erases_the_file_here); the spam filter's table from a computer to a phone in quick exchanges only, byte for byte, the good one before kept, nothing of it readable in the folder, never the language model nor the corpus (the_spam_table_travels_alone_in_every_exchange); a notes folder found empty or gone taking nothing out, filled again once its part is switched off and on (a_notes_folder_found_empty_takes_nothing_out); sealed files whole, damaged, cut, emptied, of another key (blobs); twenty versions and thirty days (history). After review: a put back takes out nothing added since, and says what it changes (a_put_back_takes_out_nothing_added_since); a quick exchange leaves notes for later, and a hurried one stops its notes where it is (a_quick_exchange_leaves_notes_for_later); notes gone at once, or a folder listing empty or unreadable, wait for a word (notes_gone_at_once_wait_for_a_word); no room holds a file back and tries again later (no_room_holds_a_file_back_and_tries_again_later); huge or endless files in the folder are never read whole (huge_or_endless_files_in_the_folder_are_never_read_whole); an edit made while receiving is never lost (an_edit_made_while_receiving_is_never_lost); a part switched off keeps nothing waiting (a_part_switched_off_keeps_nothing_waiting); nothing is written through a link nor into the sharing folder (nothing_is_written_through_a_link_nor_into_the_sharing_folder); a content used again is never lost to a sweep (a_content_used_again_is_never_lost_to_a_sweep); one exchange at a time (one_exchange_at_a_time); names a storage takes for one are never both written (names_one_storage_takes_for_one_are_never_both_written); a name with a hash travels (a_name_with_a_hash_travels); an old copy does not roll the others back (an_old_copy_does_not_roll_the_others_back); a file being written waits (a_file_being_written_waits); the doses never read the notes' memory, and its loss joins again (the_doses_never_read_the_notes_memory); what is said and left behind (what_is_said_and_left_behind); a sealed file that inflates past 64 MB stops (blobs::a_bomb_stops_at_the_largest_file); versions by reference, capped, and of gone files expiring (history::by_reference_capped_and_gone_files_expire). The folder kept small, 15 October 2026 (cargo test --release -p sioul-sync storage_tests, share_storage_tests.rs): a rebuild, a memory from before rounds said where they stand and a store joining leaving the names the others' records gave as they were, a replaced content gone two weeks after it was replaced whatever reads the rounds again; 90 days while a device runs an older Sioul, then two weeks; a content replaced on coming back after a month kept two weeks from then, the phone's version by reference opening; a device back after three weeks keeping its own last version as a copy, which puts back; a full round waiting for as much as the last one restated; a large note or paper sealed once settled, its earlier takes never, the others' change to it waiting, both kept. With the server (remote): what_edrive_sent_again_is_taken_out_once_past, past_rounds_left_on_the_server_go_at_the_next_listing, six_weeks_of_edits_what_the_server_holds_settles (above, "What the folder holds over weeks"). In the window, two test setups sharing one folder: the first alone, the second finding the folder sealed and joining, the first hearing from the second. The reviews of 5 October 2026 rechecked finding by finding on 10 October (cargo test --release -p sioul-sync correctness_tests, share_correctness_tests.rs): a round brought before the end of the one before, whose end comes late or never; a device joining from a continuing round; an older memory that moved past a round's end; a lost line dated by its own time and stamped once; the folder gone, its seal gone or no longer opening; two devices that sealed the folder apart; another folder set up with the memory kept; a list that is not text; the copy kept before sharing; a late dose's move kept against an edit of the medicine elsewhere; a draft sent never coming back; a device known by its records alone; two devices under one name. Invoices held by a computer not heard from: cargo test --release -p sioul-sync lease. Who holds rounds back, and a device that no longer counts: cargo test --release -p sioul-sync -- rounds_tests dead_phone (above, "The log", "Old contents go").
Why plain files and a log¶
The sharing carries Sioul's own files, TOML and JSON as they are on each device, through a folder any sync app carries (or Sioul itself, with a Nextcloud), as sealed records in a log. Three other ways were weighed and set aside:
- A database (SQLite on each device with a search index, then a MySQL server carrying the same records, a table of
(computer, round, seq, clock, sealed)): a server needs its port open to the Internet with TLS, a user limited to one table, and on shared hosting each device's address let in by hand, all of it the person's to run; it would have brought one place at once and transactions, which only invoice numbers need, and those take turns through leases instead (below, "Leases: one computer at a time"). Plain files stay readable in any editor, the notes folder in Obsidian or Nextcloud Notes, and need no server of Sioul's or the person's. SQLite and a search index wait until plain files are too slow (architecture.md, "Storage"). - Git inside the synced folder: git rewrites its index, references and packs in place and takes lock files, and two devices pushing to one repository through a file sync end with conflicted references: the very problems of sync apps the log avoids, each device writing only files of its own, which only grow. Git merges lines, where Sioul's data merges entry by entry (a setting, a medicine, a session). The history people want from it is the local one (above, "Earlier versions").
- Whole-folder archives of notes and papers, sealed per part: every change would send every PDF again, hundreds of megabytes over a phone's connection, and a phone's sync app (eDrive) would download the whole archive each time. Notes and papers go file by file instead, each content sealed once (above, "Notes and papers in the vault").
Not built¶
- What the server still learns (review F19, left for later): the size of each sealed file (gzip's size and 40 bytes a MiB), so a known document may be recognised by its size, and that two files hold the same content; the computers' notes to each other (
<computer>.toml) are plain and unsigned, so a forged one could keep old rounds from being removed, or show one computer more. Sizes padded to steps and notes sealed like the claims would close it. - Listing a big notes folder (review F23, in part): every file's size and time are looked at in each exchange; looking only into the folders whose time changed would spare a phone's battery.
- One series of invoice numbers per computer ("2026-A-014"): built instead is one computer numbering them at a time, the others waiting their turn (accounting.md, "Invoices on one device"). French rules accept several series only when the way the activity runs justifies them (BOI-TVA-DECLA-30-20-20-10), your accountant's call.
- Drafts in the server's Drafts folder (IMAP), for other mail clients to see: drafts travel by the log for now.
- The Porch's marks in IMAP METADATA (RFC 5464) where the server has it: by the log for now.
- Phones other than Android's: the format is plain (JSON lines, XChaCha20-Poly1305, Argon2id, HKDF for the sealed files); Sioul for Android reads and writes it (android.md), no iPhone app does yet.