2026-09-30 The 1.1.5 train, desktop and Android emulator¶
Agent-operated, for the user, on the user's computer, with a disposable server and disposable vaults. None of the user's own vaults or devices took part. This is the train's closing device run: the user journeys and the train's fault, notice and phone journeys, run on the train's builds as they were cut, each row naming its build. The phone is an emulator, not a physical Android phone. The coordinator ran the iPhone and the Windows virtual machine separately; their rows, taken from its logs, follow the lane's own.
Builds¶
Each build is named by its plugin main.js SHA-256, read back on every
device before its rows (the phone's with sha256sum on its storage). final5
to final9 were also rebuilt here from their pushed, signed commits
(195d528b, c8a7e745, 343aa1d8, cd9f629c, 5f60207d), and every file
came out byte-identical before anything was installed. final10 ran before
its two fixes were committed: the coordinator built it from 5f60207d and
those fixes, and rebuilt afterwards from their signed commit c71ff9c2, its
main.js came out byte-identical (e8562598…). Each build was
installed by a manual file copy and a reload over a vault already paired,
not a production-path install.
| Name | main.js |
Source | Rows |
|---|---|---|---|
| ffec3fb4 | 68a4e38e3fd8f89b… |
train head ffec3fb4, 111 commits over release 1.1.4 (afbf7e7) |
J1 to J11, coded 500, bare 502, revoke then Forget, the notices, #284, #245, #248 |
| final | 7887d36f30697747… |
ffec3fb4 and the #302 fix |
the coordinator's Windows rows |
| final2 | 9afd11a066cf1d94… |
train head 9fdd6a84 (#302 91dd8741, #303 33b2fac2, #304 f72860d0) |
#302, #295, #291/#300/#301, #304, #246, a natural #245, co-typing, #288, the #305 contrast, captures 1 to 3 |
| final3 | 395471e169e9c2fd… |
ccf0b66e (#305) |
#305, the #307 reproduction, the phone through a full server disk |
| final4 | 0c1b18a63809723a… |
final3 and the #307 fix | the #307 check (stopped at 5 runs), captures 4 and 5, the phone's relaunch |
| final5 | e13b1a0eb9c9ea63… |
final4, a start that met a stalled disk call made again (#307), and a phone's refusal toast taken down when the refusal ends (#308) | the #307 check, #308, speed, co-typing |
| final6 | c8f6684582727799… |
c8a7e745: final5, with one watchdog for every desktop disk call in flight instead of a timer per call (#307) |
speed, the #307 check |
| final7 | 6154acdab362602e… |
343aa1d8, and the train head ac272da7, which adds tests only: final6, with a whole-file read taking its size from the stat that proved the open (one disk call fewer per note) |
speed, the #307 check, #283, capture 6, the final7 sweep |
| final8 | 65447d546a3e8bc3… |
cd9f629c: final7, with Delete everywhere saying what it does (#309); the train head d36223a2 adds docs only |
#309, the whole-app sweep |
| final9 | 828272e4a343c64b… |
5f60207d: final8, with only a disk read let go at the bound. A change past it is awaited and logged (host decision=overrun … outcome=awaited), an answer after it is logged (host decision=late), a late read-only open's handle is closed, and a stalled open of a removal's hold is raised (review of a0dc7fc2) |
the #307 check, a deletion through the hold, speed, #311 and #310 found |
| final10 | e8562598efab41eb… |
final9, with a first sync skipping a version its feed entry no longer names as a head (#311), and an upload recording its one chunk's sid (#310); manifest.json (ed84bd8d…) and styles.css (43dd0db8…) are final9's |
speed, #311, #310, the final10 sweep |
final2 is not byte-identical to the train head's shipped plugin: three Troubleshooting strings changed after it was cut (". See Troubleshooting, …" where final2 reads " -- see Troubleshooting, …"). No row reads those strings.
- Server:
obsyncd1.1.5 on disposable journal and blob volumes, on this computer's loopback address over plain HTTP,OBSYNC_EDGE=none, blobs declared at 20 GiB and the journal at 8 GiB, except where a row says otherwise. Built atffec3fb4(8b4d13c38a8accd0…) up to the #304 row; from its second run on, the build with #304 (9d1d6e8d125242d1…). - Release 1.1.4 for comparison: built from the tag,
main.js19d3202059551d72…, byte-identical to the published release asset; itsobsyncdserved captures 4 and 5 from fresh volumes. - Desktops: three isolated Obsidian 1.13.4 instances on a macOS 27.0 laptop
(Apple M1 Max, 10 cores), each with its own
--user-data-dir, vault andHOME, driven through their DevTools ports. A few rows add a second vault window inside one of them: another device, with its own vault and plugin state. - Phone: an Android 15 emulator (system image
android-35, Google APIs, arm64-v8a; emulator 37.1.11), Obsidian 1.13.8 from the official release APK (versionCode 367), the vault in Device storage. - Load: other agent lanes shared the computer; the load average is recorded beside each timing that depends on it.
- final9 and final10 ran in a second lab, on the desktops only:
obsyncdbuilt from5f60207d(a957dd61…) on fresh volumes of the same declared sizes, three new Obsidian 1.13.4 instances, and a generated vault of 9,800 notes (21,539,336 bytes, 400 to 4,000 bytes each, in 40 folders), set up on rig A and synced to B and C. Every rig's "Deleted files" setting was the vault's own.trash.
Route¶
Neither reference route. The emulator reached the loopback server through
adb reverse; the desktops reached it directly, or through a loopback fault
hop where a row says so. No TLS terminator, no certificate on the phone.
User journeys (docs/validation.md), desktop and phone¶
The driver is page JavaScript over each app's DevTools socket (the previous
Android record's journeys.mjs, pointed at this run's devices). Timings are
on the host clock around the act and the observer's wait, resolution about
0.1 s; J8 and J9 relaunch times are the page's own clock since its window
loaded.
| # | Journey | Outcome | Time | Observed |
|---|---|---|---|---|
| J1 | Create a note on the phone | pass | 1.2 s | The desktop showed it under the same name with identical bytes (49 bytes), nothing done there. |
| J2 | Create a note on the desktop | pass | 1.2 s | The phone showed it under the same name with identical bytes (49 bytes), nothing done there. |
| J3 | Rename the vault in Obsidian's vault manager, reopen | pass | reopen to idle 2.2 s, edit 1.0 s | Still paired, the same device id, 4 active devices before and after, the folder selection unchanged, no prompt; an edit after the reopen reached the phone in 1.0 s. Renamed back afterwards. |
| J4 | Rename a selected folder | pass | 0.5 s each way | A folder of 3 notes renamed on the phone, and another renamed on the desktop: the other device lists the same 3 names and bytes under the new name, the old name gone, trash unchanged on both. |
| J5 | Move a note between two selected folders | pass | 0.3 s each way | Moved on the desktop, and on the phone: the other device shows 1 copy, in the destination only, identical bytes. |
| J6 | Move a note out of the selected folders | pass (against the plan as corrected with this record) | notice at once | Nothing was removed anywhere: the desktop kept the note under its old path with identical bytes and got no copy; the phone kept it where it was moved; trash unchanged; no deletion published. The phone said at once: "1 note moved out of the folders this device syncs, so this device stops syncing it; it stays on your other devices, and nothing was deleted. Move it back, or add the new folder under Sync folders." The plan asked it also to say that the server keeps the history; 1.1.5's notice words no longer do, so the driver failed the row. The plan's J6 now names the 1.1.5 words (docs/validation.md, in the same commit as this record). |
| J7 | New top-level folder, added to the selection | pass | 0.7 s | Before the save each side's note stayed off the other; saving the wider selection brought the desktop's note in and sent the phone's in 0.7 s; the 11 files already selected were all kept; after a restart the selection still read the two folders. |
| J8 | Quit and relaunch both | pass | desktop 1.9 s, phone 6.5 s | Each idle on its own, no prompt, still paired. |
| J9 | A large non-note tree | pass | phone 46.1 s, relaunch 2.0 s | 2,000 files of 1 KiB in 40 folders, written on the desktop in 31.3 s, all on the phone after 46.1 s (the 1.1.4 record: 487 s beside a VM and a mutation run); the desktop relaunched with the tree present was idle by 2.0 s. |
| J10 | Leave, pair the same vault again | pass | pair to idle 6.4 s | Every file (24) kept identical bytes, 0 changed, 0 added; the phone listed once among 4 active devices under a new device id; 0 chunks and 0 new versions sent (9 version posts answered "already held"); one note each way arrived (0.9 s, 1.1 s); no copy. Two earlier runs met every condition too but were graded fail by the driver, which read the device's name from a field that is empty at 1.1.5; the third run reads it by the new device id. |
| J11 | Recovery in a fresh vault | pass | recovered in 0.4 s, 2,036 files in 29.9 s | A fresh vault, a second window in one desktop instance, with the phrase read from another desktop's Show recovery phrase. A wrong setup token was refused ("This server did not accept that setup token…", server setup_refused decision=bad_setup_token). A phrase with two words swapped was refused ("recovery: the phrase fails its checksum — check the words and their order"). The real phrase restored the key, and the real token recovered the account (account_recovered, 201). The new device held all 2,036 files, as many as the desktop, 29.9 s later. No device was asked to approve anything. Unlike the plan, the other test devices stayed open: one of them was pairing the coordinator's iPhone and VM, and none of them took part. The device then left and was forgotten. |
Desktop fault journeys¶
One desktop reached the server through a loopback hop (lab-K's fault hop, extended so a flag file's one word picks the fault) that answers chunk uploads, or a faulted server's routes, itself; every other request passes through unchanged, and the hop logs how many long polls it holds at once. The desktop's status item and notices were sampled once a second; another desktop checked the note's bytes after it landed.
| Journey | Outcome | Observed |
|---|---|---|
A coded 500 io_error on every chunk upload for 150 s (#298, #299, #297) |
pass | 0 of 149 samples read offline, in each of two runs. The device read syncing 1 file until its push gave up (+93.3 s and +76.4 s), then error — Your server refused the change to "<note title>". obsync sends it again within five minutes, or at once when you select Sync now; if this stays, check your server's log., standing until the note landed. Show sync status said the same under What to do. Once the fault ended, the first run was left alone and the note landed by itself 143.9 s later; the second pressed Sync now and it landed in 3 s ("sent 1 change."). Another desktop held identical bytes both times. The hop never held more than one long poll (16 polls over three runs, each alone), and the device's own polls_in_flight read 1 at most. |
A bare 502 on every chunk upload for 120 s (#298) |
pass | offline — retrying in 59 of 119 samples (the longest stretch 42 s), alternating with syncing 1 file: a hop whose obsync is gone reads as absence, as designed. Show sync status: "Your server is not answering. obsync tries again by itself at …, and at once when this device's network comes back." The note landed 1 s after the fault ended, with identical bytes elsewhere. |
A faulted journal: every write answered 503 journal_faulted for 90 s (#295), final2 |
pass | 1.0 s into the fault the device read error — Your server hit a storage error and refuses changes until it is restarted. Restart your obsync server, then select Sync now., in 85 of 86 samples and never offline; Sync now pressed during the fault said the same in a notice, and so did Show sync status. The server was then restarted for real (TERM, drain, start) and Sync now pressed, as the words say: "sent 1 change." 3 s after the fault ended, identical bytes on another desktop. The device logged one engine decision=offline line in the run, which no status sample showed. |
A faulted nonce log: every signed request answered 503 nonce_log_faulted for 90 s (#295), final2 |
pass | The same words from 1.0 s, in 85 of 86 samples, never offline, and in Show sync status; after the restart and Sync now, "sent 1 change." 3 s after the fault ended, identical bytes elsewhere. |
A blob volume that fills mid-file (#291, #300, #301), final2, server 8b4d13c3 |
pass | The blob volume was declared at 69,143,847 bytes stored plus a 1 MiB reserve and 8 MiB of room. A 12,765,790-byte note was written, and a small note 0.4 s after it. The server stored 5 chunks (7,127,749 bytes) and refused the next 507 volume_full, the reserve kept. The small note reached another desktop 1.4 s after it was made; from 1.4 s on, all 118 samples read error — Your server is out of storage, so it refuses new changes. Free space on the server or raise its quota, then select Sync now., and so did Show sync status. The server was restarted at its ordinary capacity and Sync now pressed: the big note was sent within 3 s and held identical bytes (SHA-256) on the other desktop at 3.1 s. |
An upload refused 507 through a hop (#304), final2 |
pass | The blob volume declared at stored plus a 1 MiB reserve and 1 MiB of room, the desktop behind the fault hop passing everything through, a 3 MiB file written. On 8b4d13c3 (without #304) the hop saw the server close the connection while the upload was still being sent (EPIPE) on the first two attempts, 2 minutes apart; the third got the 507 through: the device read offline — retrying at 122.4 s and the storage words only at 124.9 s. On 9d1d6e8d (#304) the hop passed the server's 507 through, and the words came at 1.0 s with no offline sample. An earlier run on 9d1d6e8d took 31.9 s, all of it a POST /v1/chunks/exists left unanswered for its 30 s budget just after the server restarted behind the hop, before any upload; its upload's answer was the 507 too. Capture 3 was taken from that run. |
The storage words after the refused file is deleted (#305), rig C directly
on server 9d1d6e8d, the blob volume declared at stored plus 2 MiB (a
1 MiB reserve and 1 MiB of room). C wrote a 3 MiB file; the words came about
1 s later, and 2 s after that the file was deleted in Obsidian
(app.vault.delete). The status was sampled every 500 ms.
| Build | First status that is not the storage words | Clear line | Chunk PUTs after the delete |
|---|---|---|---|
| final2 (9afd11a0), contrast | +108.0 s | engine decision=cleared reason=storage at +98.0 s, from the next walk |
0 |
| final3 run 1 (395471e1) | +1.0 s | same line at +0.5 s | 0 |
| final3 run 2 (395471e1) | +1.0 s | same line at +0.5 s | 0 |
Before each delete the device logged http PUT /v1/chunks/<sid> status=507
decision=refused code=volume_full duration_ms=13–23, then push
path_class=file decision=failed reason=507 volume_full: free space is below
the watermark, at −2.0 to −2.1 s.
A Settings window closed while the disk is busy (#302, #307)¶
Obsidian 1.13 opens Settings in a window of its own. Closing it loses the
answers to disk calls in flight: three stackless Uncaught exceptions at the
moment it closes, and the calls never settle.
#302, final2. Rig C restarted, its Settings opened 1.5 to 1.6 s into the
start's walk, and the Settings window closed. In each of 3 runs the lost read
showed as scan decision=stalled call=lstat duration_ms=15000 budget_ms=15000
(15,000 to 15,002 ms), and a note written on rig B was on C's disk 15.0 s
later. Pass.
#307, found here on final3. A desktop's first sync after pairing stopped
for good. A second vault window in rig C, holding 2,029 of the server's
9,817 files, was paired from C through its Settings window, and the rig
closed that window about 2 s after the pairing. At that moment there were
three Uncaught exceptions. The feed then logged
feed decision=stalled waited_ms=110004 budget_ms=110000 poll_ms=none
reading=0 answered=1 last_seq=3580 in_flight=3 pushing=0 chain=page:109987
behind=2 pulls=300 step=apply:3582:109216 … and scan decision=stalled
waited_ms=120002 budget_ms=120000. The status read syncing 300 files from
then on, and the device made no further request, so it never learned that
it had been revoked meanwhile. The coordinator read the stuck page over
DevTools: the apply of seq 3582 was comparing a file the device held with
the same bytes, and its disk reads had lost their answers. #302 bounds only
the walk's reads. #307 bounds every desktop disk call at the filesystem
seam.
The reproduction (apply_stall.mjs), per run:
- The previous device is revoked and forgotten.
- The window is closed, its vault reset to the same 2,029 files, and the window reopened: a fresh plugin, so a stuck run never carries over.
- It is paired from C through its Settings window.
- That window is closed at a random moment.
- The device is followed until its status is idle with its feed at C's sequence, sampling every second.
A run is WEDGED when it is not idle after 180 s and has made no progress
for 60 s. It is SLOW when it is not idle after 180 s but still making
progress; it is then followed to idle, up to 600 s. It is RECOVERED when it
logged a stall line (feed decision=stalled, scan decision=stalled,
host decision=stalled, feed decision=retry, or, from final5, the start's
engine decision=stopped reason=start_stalled) and then reached idle, and
CLEAN otherwise. A clean first sync here downloads about 7,800 files and
takes about 200 s, so SLOW is the clean first sync's usual class.
The window is closed at approval plus a random delay, but never before the device reports itself paired, so every close landed 102 to 355 ms after that, at the start of the first sync.
| Build | Run | Class | Close after pairing | Idle after the close | Stall lines | Uncaught at the close |
|---|---|---|---|---|---|---|
| final3 | 1 | WEDGED | +105 ms | never | 0 | 3 |
| final3 | 2 | SLOW | +355 ms | 219 s | 0 | 0 |
| final3 | 3 | SLOW | +103 ms | 201 s | 0 | 0 |
| final3 | 4 | CLEAN | +102 ms | 172.9 s | 0 | 0 |
| final3 | 5 | SLOW | +206 ms | 192.8 s | 0 | 0 |
| final4 | 1 | CLEAN | +103 ms | 139.8 s | 0 | 0 |
| final4 | 2 | CLEAN | +102 ms | 151 s | 0 | 0 |
| final4 | 3 | WEDGED | +102 ms | never | 1 | 3 |
| final4 | 4 | CLEAN | +103 ms | 138.7 s | 0 | 0 |
| final4 | 5 | SLOW | +103 ms | 199.2 s | 0 | 0 |
final3, run 1 was the stopped first sync described above.
final4, run 3 met a lost read again, and final4's bound caught it:
host decision=stalled call=lstat duration_ms=15002 budget_ms=15000, 15.0 s
after the close. But the lost call was the engine's start's, and in the same
millisecond the device logged engine decision=stopped
reason=start_failed code=Error. A start that fails on a local fault was
treated as a refusal, which nothing asks again by a timer (#129). The
status read error — This device's disk did not answer in time. obsync
tries again by itself., and the device made no request and logged no line
for the 165 s that followed. The words promised a retry that never came.
The final4 check was stopped after run 5. final5 starts again after the
reconnect's pause: engine decision=stopped reason=start_stalled
code=disk_stalled, then engine decision=retry_scheduled. A refusal still
gets no timer.
final5, all 10 runs: 5 closed at approval plus up to 3 s, and 5 at
pairing plus up to 3 s, so the second half's closes landed while the first
sync was applying. 0 WEDGED: 6 CLEAN and 4 RECOVERED. Every run's device
read back e13b1a0e. The first syncs finished 133.5 to 160.6 s after the
close (final3: 172.9 to 219 s).
| Run | Close after pairing | Applying at the close | Class | Idle after the close | Lines |
|---|---|---|---|---|---|
| 1 | +103 ms | no | RECOVERED | 160.6 s | the start's lost read: at +15.0 s host decision=stalled call=lstat duration_ms=15001 budget_ms=15000, engine decision=stopped reason=start_stalled code=disk_stalled, engine decision=retry_scheduled attempt=1 delay_ms=5000 status=0; at +21.0 s engine decision=resumed attempt=1 |
| 2 | +102 ms | no | CLEAN | 135.6 s | none |
| 3 | +102 ms | no | CLEAN | 133.6 s | none |
| 4 | +102 ms | no | CLEAN | 134.8 s | none |
| 5 | +103 ms | no | CLEAN | 133.6 s | none |
| 6 | +2,477 ms | seq 2856, 2,583 files pulled | RECOVERED | 153.6 s | the apply's lost read: at +15.0 s host decision=stalled call=lstat duration_ms=15004 budget_ms=15000, feed decision=retry reason=disk_stalled status=unchanged retry_ms=5000 |
| 7 | +2,194 ms | seq 2182, 1,922 files pulled | RECOVERED | 150.8 s | the same two lines (duration_ms=15004) |
| 8 | +460 ms | no | CLEAN | 135.6 s | none |
| 9 | +2,255 ms | seq 2394, 2,131 files pulled | RECOVERED | 152.8 s | the same two lines (duration_ms=15003) |
| 10 | +1,509 ms | seq 1042, 793 files pulled | CLEAN | 133.5 s | none |
While run 1's start waited, the status read error — This device's disk
did not answer in time. obsync tries again by itself. This time the words
were true. Pass.
final6 moved the bound to one watchdog that looks once a second, so a
stall now lands between 15 and 16 s. 10 runs in the same shape: 0 WEDGED,
6 CLEAN and 4 RECOVERED. Runs 1 to 5 were CLEAN, idle 140.8 to 147.8 s after
the close. Runs 6 to 9 closed during the apply, after 200 to 3,267 files
were pulled. Each met a lost read (host decision=stalled call=lstat
duration_ms=15667, 15996, 15078 and 15721, budget_ms=15000), logged
feed decision=retry reason=disk_stalled status=unchanged retry_ms=5000,
and was idle 160.2 to 163.8 s after the close. Run 10 was CLEAN (140.3 s).
Pass.
final7, 5 runs, all closed at pairing plus up to
3 s (the shape that met a stall in 3 and 4 runs of 5 on final5 and final6):
0 WEDGED, 3 CLEAN and 2 RECOVERED. Both paths met a lost read again and came
back on their own:
- Run 2 closed during the apply, after 3,149 files were pulled:
host decision=stalled call=lstat duration_ms=15259 budget_ms=15000, then
feed decision=retry reason=disk_stalled, and idle 158.6 s after the
close.
- Run 3 closed during the start: host decision=stalled call=lstat
duration_ms=15127, engine decision=stopped reason=start_stalled
code=disk_stalled, engine decision=retry_scheduled attempt=1
delay_ms=5000, and at +21.0 s engine decision=resumed attempt=1, then
idle 159.4 s after the close.
- Runs 1, 4 and 5 were CLEAN, idle 139 to 140.7 s after the close.
Pass.
final8 was not taken through this check again, nor through the speed
rows below. Its one change over final7 is three lines in
plugin/src/sync/engine.ts: one notice after Delete everywhere (#309). It
touches neither the disk seam nor the read path.
final9 changes the disk seam (review of a0dc7fc2). Only a read is let
go at the bound. A change past it (an open for writing, a folder made, a
rename, a link or unlink, a folder removed, a time set, a handle's write or
sync) logs host decision=overrun … outcome=awaited and is still awaited,
and any answer after the bound logs host decision=late … outcome=answered
or refused. 5 runs in the second lab, the window closed at pairing plus up
to 3 s; the vault held 2,029 of the server's 9,800 files, and a first sync
there takes about 140 s.
| Run | Close after pairing | At the close | Class | Idle after the close |
|---|---|---|---|---|
| 1 | +1,039 ms | applying, 17 pull lines | CLEAN | 140.6 s |
| 2 | +889 ms | applying, 14 pull lines | CLEAN | 140.7 s |
| 3 | +1,955 ms | applying, 102 pull lines | CLEAN | 137.6 s |
| 4 | +2,601 ms | applying, 153 pull lines | CLEAN | 139.7 s |
| 5 | +683 ms | applying, 3 pull lines | CLEAN | 136.7 s |
0 WEDGED. Every run's device read back 828272e4. No run met a lost read:
0 stall, overrun or late lines, and 0 exceptions, in all five. So the
new lines never fired here, and these runs show only that nothing went
backwards. Pass.
What #307's bound costs¶
Rig B, 9,816 files, on a machine the other lanes kept quiet (load average
3.3 to 4.8). Each block was taken right after B was restarted on its build,
with nothing changed in the vault. Each press is that press's own
sync_now decision=drained … examined=9815 … read=9815 bytes=19452407
duration_ms=… line: a desktop's Sync now reads every file. A walk is
host.scan timed in the page.
| Order | Build | Sync now presses | Median | Walks | Median |
|---|---|---|---|---|---|
| 1 | final3 | 3,199, 4,592, 4,807, 4,640, 4,613 ms | 4,613 ms | 249, 246, 266 ms | 249 ms |
| 2 | final5 | 5,156, 5,108, 5,296, 5,106, 5,061 ms | 5,108 ms | 251, 248, 252 ms | 251 ms |
| 3 | final3 | 4,764, 4,625 ms | 4,695 ms | - | - |
| 4 | final5 | 5,219, 5,108 ms | 5,164 ms | - | - |
A press costs about 0.5 s more on final5, about 10 %, or about 48 µs for each of the 9,815 reads. Both builds reproduced within 100 ms when taken again. The walk did not change. final5 differs from final3 only by #307's bound around every desktop disk call, and by two changes off the read path (the start retry and #308), so the bound is the likely cost. Earlier the same day, at load 8 to 12, final3 took a median of 5,038 ms per press and 253 ms per walk.
final6 moved #307's bound to one watchdog for every call in flight, and was measured the same way, at load 3.6 to 5.1, on 9,813 files (three test notes had been removed):
| Order | Build | Sync now presses | Median | Walks | Median |
|---|---|---|---|---|---|
| 1 | final3 | 4,718, 4,673, 4,664, 4,615, 4,602 ms | 4,664 ms | 250, 236, 250 ms | 250 ms |
| 2 | final6 | 4,785, 4,719, 4,806, 4,716, 4,706 ms | 4,719 ms | 253, 239, 218 ms | 239 ms |
| 3 | final3 | 4,630, 4,585 ms | 4,608 ms | - | - |
| 4 | final6 | 4,792, 4,784 ms | 4,788 ms | - | - |
Over the 7 presses each, final6's median is 4,784 ms and final3's is 4,630 ms: 154 ms, about 3 %, slower. In 47 of the 49 pairs the final6 press is the slower one. That is about 16 µs a read, a third of final5's 48 µs. The walk did not change.
final7 takes a whole-file read's size from the stat that proved its open, one disk call fewer per note. Measured the same way, at load 3.4 to 5.7:
| Order | Build | Sync now presses | Median | Walks | Median |
|---|---|---|---|---|---|
| 1 | final3 | 4,696, 4,588, 4,667, 4,615, 4,581 ms | 4,615 ms | 248, 256, 248 ms | 248 ms |
| 2 | final7 | 4,704, 4,695, 4,689, 4,575, 4,658 ms | 4,689 ms | 228, 230, 227 ms | 228 ms |
| 3 | final3 | 4,621, 4,622 ms | 4,622 ms | - | - |
| 4 | final7 | 4,758, 4,658 ms | 4,708 ms | - | - |
Over the 7 presses each, final7's median is 4,689 ms and final3's is 4,621 ms. That is inside noise at this sample, with a point estimate of +68 ms (+1.5 %). In 36 of the 49 pairs the final7 press is the slower one: U = 13, against a critical 8 for p < 0.05 with 7 presses against 7. final7's fastest press, 4,575 ms, was the fastest of all 14, and its walk was 20 ms faster. Across the four builds: final3, then final5 at +0.5 s (about 10 %), then final6 at +154 ms (47 of 49 pairs slower), then final7 inside noise.
final9 awaits a change past the bound instead of letting it go; a press is
reads, through the same watchdog. final10's two fixes do not run for a press
over a vault the device already holds. Both were measured the same way in the
second lab, on rig B with 9,800 files (21,539,336 bytes), against final8 as
rebuilt from cd9f629c. A press there takes about 3.2 s, so these numbers
compare with each other and not with the tables above.
| Order | Build | Sync now presses | Median | Walks | Median |
|---|---|---|---|---|---|
| 1 | final8 | 3,321, 3,248, 3,201, 3,678, 3,209 ms | 3,248 ms | 230, 270, 224 ms | 230 ms |
| 2 | final9 | 3,260, 3,244, 3,171, 3,322, 3,156 ms | 3,244 ms | 219, 233, 239 ms | 233 ms |
| 3 | final8 | 3,245, 3,505 ms | 3,375 ms | - | - |
| 4 | final9 | 3,308, 3,384 ms | 3,346 ms | - | - |
At load 4.4 to 8.3, final9's median over its 7 presses is 3,260 ms and final8's is 3,248 ms: +12 ms (+0.4 %). The final9 press is the slower one in 20 of the 49 pairs, U = 20. Inside noise.
| Order | Build | Sync now presses | Median | Walks | Median |
|---|---|---|---|---|---|
| 1 | final8 | 3,270, 3,228, 3,191, 3,337, 3,200 ms | 3,228 ms | 238, 205, 221 ms | 221 ms |
| 2 | final10 | 3,259, 3,230, 3,215, 3,331, 3,208 ms | 3,230 ms | 243, 207, 222 ms | 222 ms |
| 3 | final8 | 3,277, 3,195 ms | 3,236 ms | - | - |
| 4 | final10 | 3,216, 3,205 ms | 3,211 ms | - | - |
At load 3.2 to 6.5, final10's median is 3,216 ms and final8's is 3,228 ms: −12 ms (−0.4 %). The final10 press is the slower one in 26 of the 49 pairs, U = 23. Inside noise; the walk did not change.
A deletion from another device, through the hold (final9)¶
A deletion of a version this device holds is applied through a hold: the
note is moved into a hidden folder made beside it (.obsync-gone-…),
compared with a hold on its bytes (.obsync-hold-….tmp), and only then
handed to the vault's own deletion under its own name. final9 raises a
stalled open of the hold instead of treating it as gone. Driven on rig A over
DevTools (row_delete.mjs), with B's disk as the other device; every vault
(its .trash included) was walked for .obsync-gone-* and .obsync-hold-*
after each step.
| Journey | Outcome | Observed |
|---|---|---|
| A note made on A, deleted on B, 3 runs | pass, 3 of 3 | Each note was on B's disk 1.0 s after it was made. After B deleted it, it left A's disk 0.6 s later, in each run: host path_class=file decision=trashed bin=local at +0.55 to +0.56 s, the line the hidden name's removal writes, then pull path_class=tombstone decision=deleted at +0.58 to +0.59 s. Each note was in A's .trash under its own name. |
| 6 notes deleted on A, answered Delete everywhere | pass | The question rose 0.8 s after the deletes, and B still held all 6. On Delete everywhere, "obsync: deleting 6 notes on your other devices too." showed, and B held none 0.8 s after the press. |
After every step: 0 .obsync-gone-* or .obsync-hold-* names on any of the
three vaults, and 0 host decision=stalled, overrun or late lines on A
or B.
A first sync over the server's history (#311)¶
A device with no record of a file replays the feed from its start, and the
feed holds every version the server keeps. On final9 the device wrote each of
them in turn, and a note deleted elsewhere was downloaded, written, and then
moved to that device's trash. The fresh device in these rows is the second
vault window in rig C: its vault emptied, its plugin state removed, its
previous device revoked and forgotten, its "Deleted files" its own .trash,
and then paired from C.
Found on final9, one note. A 4 MiB note (4,194,304 bytes) was written on
A. It was on B's disk 1.2 s later, then deleted on A, and gone from B and C
0.6 s after that. Then the fresh device was paired, twice (firstsync.mjs):
- Run 1: idle 174.9 s after pairing. The note's chunk was asked for at
173.8 s, and the note was in the device's .trash at 174.7 s. The server
sent 27,680,587 bytes of chunks for a vault of 21,539,336 bytes.
- Run 2, with a second such note: idle 177.0 s after pairing. The note stood
at its name at 176.8 s and was in .trash at 176.9 s. 31,875,092 bytes,
4,194,505 more.
After each run the device's .trash held every note deleted on that server
before the pair: the 9 from the deletion journey above, and the 4 MiB notes.
final9 against final10, with history. Before each pair, rig A made the
history (firstsync_history.mjs): 6 notes of 32 KiB, created, on B's disk,
then deleted (answered Delete everywhere); and one note edited 5 times, each
edit on B's disk before the next. The feed, read from C just before the
pair, says which entries are no longer a head of their file. The device's
disk was sampled every 50 ms until it was idle.
| final9 | final10 | |
|---|---|---|
| Superseded versions with content in the feed at the pair | 22 | 34 |
pull … decision=applied |
9,823 | 9,801 |
| Live notes after idle, identical to B | 9,801 | 9,801 |
pull decision=skipped reason=superseded_in_feed |
none | 34: each a superseded entry, of the file the feed names, and none left over |
| Tombstone lines | 17 tombstone decision=deleted |
none |
The device's .trash |
17 notes | empty |
| The edited note on its disk | v0, v1, v2, v4, v5 seen in turn (v3 fell between samples) |
v5 only |
| Chunk sids asked, in 157 batches | 9,823, all 12 of the history's | 9,801, 1 of the history's 12 (v5) |
| Chunk bytes the server sent | 32,270,762 | 23,517,054 |
| Idle after pairing | 178.1 s | 173.5 s |
final10 had 12 more versions to skip than final9 had written: final9's own
history, on the same server, whose edited note was deleted after its run. On
final9 each deleted note stood on the device's disk for 0.3 to 0.5 s before
it reached .trash. On final10 none of them was ever seen on its disk. Pass
on final10.
The repair walk after an upload (#310)¶
The repair walk asks the server about each note's one chunk by the sid the
device remembers, 4,096 to a request. A record with no sid is read back from
the server instead, one a second (repair decision=checked budget_sids=64
budget_chunks=1). A walk starts with the engine, and then every 6 hours. No
line marks its start; repair decision=walked ends it.
Found on final9, on rig A, the device that uploaded the 9,800-note vault
at setup. About 25 minutes after setup, A remembered a sid for 7,680 of its
9,800 records, 1,220 of them read back by its walk so far, while B and C,
which pulled every note, held 9,800 of 9,800. From the start of that walk at
04:45:15Z, A read one version back and asked one chunks/exists a second:
2,398 reads in 41.0 min, ending
repair decision=walked batched=4745 requests=2 missing=0 learned=2398
next_ms=21600000 duration_ms=2458776. A walk takes up the records that
exist when it starts. This one asked about 4,745 remembered chunks, and
each of its 2,398 reads learned a sid, so none of A's records was read
without learning one. 942 records it never took up, made after it began,
still had no sid when it
ended, and the walk that started when A restarted on final10 began reading
those back, for the first time.
final9 against final10. 2,000 one-chunk notes (1 KiB each) were written
at once into one desktop's vault (sid310.mjs). Once that desktop was idle,
its records were counted, it was restarted so that a walk began, and its
requests were counted for the next 5 minutes from the server's log.
| final9, rig C | final10, rig B | |
|---|---|---|
| Recorded and idle | 83.1 s (load 7.8) | 77.6 s (load 4.9) |
| All on another desktop's disk | 109.3 s (load 7.6) | 102.7 s (load 4.6) |
| New records without a sid, in memory and in the data file after the restart | 473 of 2,000 | 0 of 2,000 |
| Version reads in the 5 minutes after the restart | 290, with 293 chunks/exists; 183 records still without a sid at the end |
0, with 3 chunks/exists; the walk ended 4.5 s after the restart: repair decision=walked batched=11800 requests=3 missing=0 learned=0 … duration_ms=3121 |
| Load in those 5 minutes | 2.5 to 9.9 | 2.8 to 5.8 |
Pass on final10.
Desktop behaviour¶
| Journey | Outcome | Observed |
|---|---|---|
| Three devices, two typing into one note (#227, #278, #279), final2, 3 runs | pass, 3 of 3 | The committed driver scripts/validation/cotype-live.mjs at 60000 200 60000 beside six CPU burners (load 7 to 32): B and C typed for a minute each, and A showed the note and typed nothing. Every run: every disk held exactly the expected text, each open editor showed its disk, both typists' every token was kept in order, and 0 conflict copies. A merged 95, 92 and 92 times with 0 unmerged. No device showed a notice, and every device ended idle. |
The same, final5, with the driver as committed at 195d528b (it finds a window by its title and passes the page its values as arguments), 3 runs |
pass, 3 of 3 | The same shape and settings beside six CPU burners (load 4.3 to 10.1). Each typist typed 298 characters in 60 s. Every run: every disk held exactly the expected text, each open editor showed its disk, both typists' every token was kept in order, and 0 conflict copies. A merged 59 times in each run, with 0 unmerged. No device showed a notice, and every device ended idle. Each run's note was deleted afterwards and was gone from the other two disks within 1.0 s. |
| A laptop wake: three desktops hidden, frozen 45 s, shown (#288), final2, 2 runs | pass, 2 of 2 | Rigs A, B and C were minimized, their renderers stopped (SIGSTOP, pid-verified) for 45 s and continued, and their windows restored; each device's status was sampled every second for 150 s. On every device, in both runs: 0 samples read offline (of 150 and 149), 0 engine decision=offline lines, idle at the end. |
| A minimized desktop sends a folder rename at once (#283), final7, 3 renames each way | pass | Rig A was restarted and minimized before any work, and stayed so for 30 s: its page reported hidden, and an idle 100 ms timer took 624, 1,000, 999, 1,000 and 1,001 ms (a 1,000 ms timer took about 2,000). A folder of 20 notes was renamed on A three times. Each time A logged host decision=throttle_lifted reason=work, its 100 ms timer ran at 100 to 113 ms during the work, and B's disk held all 20 under the new name 0.9 s later. Shown, the same renames took 1.0 s each, with idle timers at 100 to 102 ms. The first try ran the shown condition first. Its minimized half then reported itself visible and unthrottled, as the 2026-09-29 speed record found for a window lifted while shown, so the run was repeated minimized-first. The folder was deleted afterwards: obsync held the 20-note deletion and asked whether to delete them on the other devices too, the run answered Delete everywhere, and B's disk was clear 1.5 s later. |
Devices, notices and the phone¶
| Journey | Outcome | Observed |
|---|---|---|
| Revoke a device, then Forget it (#247, #268) | pass | A throwaway device (a second vault window, paired from a desktop) was revoked from that desktop's settings, through the row's button and its confirmation. The revoked device's next request was refused (403 device_revoked), and within 25.1 s it read "This device was removed from your server. Your notes and vault key are safe here. Pair it again from a device that still syncs…". The other devices went on. The device list's summary read "5 devices on this account, and 3 revoked" before the revoke, "4 … and 4 revoked" after it, and "4 … and 3 revoked" after Forget; the revoked rows sit behind one "N revoked devices" row. Check said "reached "obsync", 5 devices." and then "4 devices."; the dashboard Overview's device count read 5 and then 4, equal to the working devices each time. The admin API still lists the forgotten row, marked archived, and the dashboard folds it, as designed. The forgotten device kept saying it was removed. Paired again from the same desktop, it synced; both pair dialogs closed by themselves on approval. |
| Twenty Sync now presses (notices) | pass | Twenty presses over 11.6 s on a desktop. A toast joins the presses that arrive while it is on screen and is not extended by them, so successive toasts counted "(2 times)" … "(8 times)", and 1 was on screen at the end. Recent held one line: "nothing to send; this device is up to date (20 times)." |
| Recent never carries a match code | pass | After four pairings (the phone three times, a desktop window once), the Recent lists of three desktops and the phone held no six-digit code. The approver's lines name the device and say it is paired; the phone's pairing line is "(3 times)". |
| The pair dialog closes on approval | pass | On a desktop pairing, both dialogs were gone within a second of the claimant holding the key, with nothing pressed. |
| The security warning's alert icon | not reached | It shows only when the server answers a device's recovery registration with recovery_mismatch, which needs another key registered for the vault. No honest path reaches that on one lab account; a simulated answer is what the 2026-09-29 records used. |
| A note deleted elsewhere while the phone holds a folder under its name (#284) | pass | The note's name on the phone's storage was turned into a folder holding a note, outside Obsidian; the desktop deleted the note. The phone logged pull path_class=manifest decision=refused reason=not_a_file, and the folder and its note stayed, bytes intact (4.1 s). The phone re-sent the same deletion, which the server deduplicated, and later synced the note inside the folder as a new note. |
| Leave's count with a stale file index (#245) | pass | Three synced notes' entries in Obsidian's in-memory index were set to 0 bytes in the page, as Android's late writes leave them, while disk and record agree. Leave's count listed 0 unsent notes (0 before), in 0.1 s. The phone logged three list decision=stale_index size_index=0 size_disk=<n> lines and list decision=confirmed suspects=3 stale=3 files=2029 duration_ms=5. Leave's dialog opened and was cancelled; the phone stayed paired. |
| Leave's count after a real late write (#245), final2 | pass | After the phone downloaded the 7,700-note fixture, one file Android wrote late was left in the phone's index at 8,192 bytes against 17,469 on disk. Leave's count said 0 unsent; the phone logged list decision=stale_index size_index=8192 size_disk=17469 …, then list decision=confirmed suspects=1 stale=1 files=9814 duration_ms=3. |
| Sync now with nothing to send on a large phone vault (#246) | pass | Android 15 emulator, final2, 9,814 files: the 7,700 fixture notes, which rig B uploaded in 291 s and the phone, A and C all held 490 s later, and 2,114 others. Five presses took 850, 969, 888, 965 and 890 ms, each the press's own sync_now decision=drained queued=0 in_flight=0 follow_up=0 examined=0 listing=readdir folders=120 files=9814 differing=0 read=0 bytes=0 duration_ms=… budget_ms=10000 line. The status read idle 318 to 330 ms after each press and never counted a file. Load average 7.1 to 9.6. |
| A phone stopped mid-download, with the mark unsaved (#248) | pass, twice | Every phone write under a probe folder landed empty, and the first never returned, so no save followed. The app was then stopped (am force-stop) and started again. For a note new to the phone, and for an edit of a note it held: the restarted phone logged reconcile decision=held reason=unfinished_download files=1 unverified=0, posted 0 versions of the note, and was idle in 3.2 s. Both devices then held the desktop's text, and the desktop had 1 file under the probe folder: no conflict copy. |
| The server's disk fills, then gets room again (phone, final3) | pass, with a finding (#308) | Not staged: the computer's disk filled under the day's other work at about 17:37Z. The server could not record request nonces (nonce_log decision=refused io=StorageFull, 17:36:52Z to 17:37:03Z) and answered 507 storage_full to the four devices that asked, never a lie about readiness. The phone's toast read "obsync: Your server is out of storage, so it refuses new changes. Free space on the server or raise its quota, then select Sync now." at 17:36:57.7Z. The emulator itself stood paused while the disk was full (its qemu stops the guest when a host write fails; the guest logged no I/O error). Once the disk had room and the emulator was resumed, the phone's first request was answered 200 2.0 s later, and its status item read synced by itself, nothing pressed. The toast stayed, contradicting it: still on screen after 20 s untouched, after a Sync now that drained (sync_now decision=drained … differing=0 … duration_ms=873), and 60 s after that press. Only a tap closes an error toast, and nothing closes it when the error ends. |
| A refusal's toast goes when the refusal ends (#308), phone, final5 | pass | The server's blob volume was declared at its stored bytes plus 2 MiB (a 1 MiB reserve and 1 MiB of room). The phone wrote a 3 MiB file and was refused 1.1 s later (http PUT /v1/chunks/<sid> status=507 decision=refused code=volume_full). From 1.3 s, its status read error — Your server is out of storage, so it refuses new changes. Free space on the server or raise its quota, then select Sync now. and its toast said the same. The server was then restarted at its ordinary capacity. The phone's first request after that was answered 0.55 s later. The words and the toast stood on, rightly: they speak of a file still unsent. The phone sent it by itself 70.4 s later, nothing pressed, and logged engine decision=cleared reason=storage at +70.6 s. The toast left in the same 250 ms sample in which the status item turned idle [synced], between +71.4 and +72.2 s. Through two server restarts, the desktops' first requests were answered 0.6 to 3.4 s after the server was ready. The full-disk case of the row above (a 507 on every request) was not staged again: its refusal ends through the same status path, which plugin/test/status-ui.test.mjs covers for any refusal that ends (the toast hidden, the Recent line kept). |
| Delete everywhere says what it does (#309), desktop, final8 | pass | Rig A made 6 notes in a test folder, and rig B's disk held all 6 1.2 s later. A then deleted the 6 back to back. obsync held them (watch decision=held reason=bulk_deletion files=6 held=6 floor=5) and asked 0.6 s later: "obsync: you deleted 6 notes (in reconcile decision=confirmed reason=bulk_deletion queued=6 and notice decision=shown kind=confirm stays_ms=4000, the question left, and the toast "obsync: deleting 6 notes on your other devices too." rose within the 100 ms sample. B's disk held none of the 6 0.8 s after the press. Show sync status's Recent, newest first, then read "deleting 6 notes on your other devices too." above the question, so it no longer ends on a question already answered. A screenshot of it, with the server address and the device's name and id blurred, is kept with the run's outputs. The folder was deleted afterwards, and B's disk was clear 0.5 s later. |
On the same screen, Obsidian's own "Indexing vault…" stood at 114 of 7,815 notes. It had been stuck since the app started at 17:03Z, before the disk filled. Obsidian mobile's metadata worker held one core (98.6 %; 34 min of CPU, nearly all of it before the pause) on one note: the 12,765,790-byte note of 76-character lines from the blob-volume row, which had synced everywhere once room came back. Every note queued after it waited. Desktop Obsidian indexed the same note as one section, at once. obsync was not involved (the plugin starts no worker) and stayed idle throughout. Once the note was deleted and the phone moved to final4 and relaunched, obsync was idle 3.4 s after the launch, and Obsidian indexed all 7,813 notes in 51.5 s.
Captures for Troubleshooting¶
Six captures (the sixth in two parts), asked for by the user, for the
Troubleshooting entries named beside them. Each is window content only
(DevTools Page.captureScreenshot, the window never brought forward),
cropped to what the words describe, from
the isolated rigs in the dark theme. Addresses other than the reserved
lan.example.test, and device names and ids, are blurred in the image, or
the device carries a role name. Each shot was refused while a
secret-bearing dialog, code or field was on screen. They are kept with the
run's other outputs, not in this repository.
| File | Shows | Build | SHA-256 |
|---|---|---|---|
server-url-refused.png |
Settings with Server URL http://lan.example.test, and the notice "obsync: Use your server's https address. Plain HTTP would send the setup token and every request unencrypted; it is accepted only for this computer itself (localhost or 127.0.0.1)." |
final2 | 7a8ee4df29c1d41dfb12b745ab14939a6b3e37c585b067a7b4f8fcc627b56e46 |
check-server-url-not-saved.png |
The same field after Check, and the notice "obsync: Server URL was not saved: Use your server's https address. …" | final2 | 1a6cf448100d0e81b117fee494c640dcdca220ccc50f93465264724709a64149 |
server-out-of-storage.png |
Show sync status during the #304 row: What to do and State both "Your server is out of storage, so it refuses new changes. Free space on the server or raise its quota, then select Sync now.", with Retry now | final2, server 9d1d6e8d |
1a762fd911ae27c2686273b56647a5e8fa7f24db2c043485136fa0e3149cf6ef |
pair-older-server.png ("Pairing says to update your obsync server") |
Pair a new device on a 1.1.5 desktop whose server is 1.1.4: "Your obsync server runs a version older than 1.1.5, or does not say which, so no code was made. Update your obsync server to 1.1.5 or later, then pair again. See Troubleshooting, "Pairing says to update your obsync server"." | final4, server release 1.1.4 | fa700d838cd815c6fb933799e77a5de608cd3fdb4535ea007b4b70f708d2682f |
forget-too-old.png ("A device could not be forgotten") |
Forget pressed on a revoked device, same server: "obsync: Old laptop was not forgotten: your server is too old to forget devices. Update it to obsync 1.1.5 or later, then try again." | final4, server release 1.1.4 | d4b61ed0ee0b11b7c455effd478af67d57750e75230049ff27e6fb20657ed661 |
device-forgotten.png ("This device was removed from your server") |
Show sync status on a device that another device revoked and then forgot. What to do: "This device was removed from your server. Your notes and vault key are safe here. Pair it again from a device that still syncs: obsync settings, Pair this device.", with Pair again, and State the same after "error —". The server address and the device's name and id are blurred | final7 | 3baa14c4262bf2ff3618a2f1767a5eff00bdf057af333029137b0cea7dfb8c7e |
device-forgotten-settings.png (the same entry) |
The same device's settings: Pairing, "This server no longer recognises this device. Your local notes and vault key are safe. In obsync settings, pair from a syncing device, or use Setup or recover with this server's setup token and this vault's recovery phrase.", and Setup or recover with its setup-token field empty | final7 | b76b6c2cec7ebb210c00c97b70b241f525f5b918e161d76c00e34eb5e8486b06 |
For captures 4 and 5 a fresh 1.1.4 server was started on new volumes, and two second vault windows in one desktop were set up and paired on the 1.1.4 plugin. The one that stayed was then moved to final4, and the other was revoked from it. Everything was removed afterwards.
For capture 6, a second vault window in a desktop was set up from 2,029 files on final7, paired from that desktop, and left to finish its first sync (149 s). It was then revoked and forgotten through the plugin's own calls. Its vault was emptied afterwards.
Whole-app sweep¶
On final7, on the three desktops and the phone, after every row above except #309's: the status item, the notices on screen, Show sync status, the obsync settings tab (on a desktop, Obsidian 1.13's own Settings window) and the console, with a screenshot of each, refused while any secret-bearing dialog or field was open.
- Every device's status item read
obsync: idle [synced], and no notice was on screen. - Show sync status, on every device: State idle, vault key present, 9,813 files tracked, 82.5 MiB, Remote only 0, all at the same feed sequence. The phone showed its mobile ceilings (512 MiB per file, 50 GiB in all); the desktops showed unlimited.
- The phone and one desktop still offer the recovery phrase's "Show and confirm": these test vaults never confirmed it.
- The settings tabs held no error, refusal or stale words. The only matches were the help text on removed folders and on which notices show.
- Each console replay covered 723 to 1,000 messages. On every device it held 0 obsync warnings, 0 error-level lines and 0 uncaught exceptions.
- One desktop's Recent list still held the #283 run's bulk-deletion question, worded as open ("Delete them on your other devices too? They stay there until you choose."). It had been answered with Delete everywhere, and no line in Recent says so. This was filed as #309 and fixed in final8 (see its row).
- The red icon in each desktop's status bar, beside obsync's check, is Obsidian's own Sync core plugin, enabled and "Uninitialized" in these test vaults. It is not obsync's.
On final8, the same sweep after the #309 row, on all four devices:
- Every status item read obsync: idle [synced], and no notice was on
screen.
- Show sync status: 9,813 files tracked, all at the same feed sequence.
- The desktop that ran #309 listed the answer, "deleting 6 notes on your
other devices too.", above its question in Recent. The others read
"Nothing since obsync started."
- The settings tabs held no error, refusal or stale words.
- Each console replay covered 65 to 101 messages, since every app restarted
for final8. Each held 0 obsync warnings, 0 error-level lines and 0
uncaught exceptions.
On final10, the final build, the same sweep on the second lab's three
desktops, after every row above:
- Every status item read obsync: idle [synced], and no notice was on
screen.
- Show sync status: 9,800 files tracked, 20.5 MiB, Remote only 0. The feed
held no entry past the sequence each device showed.
- The desktop that ran the last #310 row listed the answer, "deleting 2000
notes on your other devices too.", above its question in Recent. The
others read "Nothing since obsync started."
- The settings tabs held no error, refusal or stale words.
- The console replays covered 27, 58 and 1,000 messages, each with 0 obsync
warnings, 0 error-level lines and 0 uncaught exceptions.
Run by the coordinator: iPhone and Windows¶
Copied from the coordinator's logs of the same day, against this run's
server (8b4d13c3) and desktop rig A as the peer.
iPhone. The user's iPhone, driven through iPhone Mirroring from the
Mac; Obsidian mobile with a disposable vault (obsync-train-115), plugin
68a4e38e (ffec3fb4). The #302 fix changes no mobile code path. The server
was reached over HTTPS through a quick tunnel. Afterwards Obsidian was
switched back to the user's own vault, untouched, and the tunnels were
stopped.
| Journey | Outcome | Time | Observed |
|---|---|---|---|
| Install | pass | - | The vault ZIP was downloaded in Safari, unzipped in Files and opened in Obsidian. Community plugins lists "Self Hosted Private Sync v1.1.5, By Samuel Naranjo", enabled. |
| Check | pass | < 5 s | Server URL pasted, then Check: "reached your obsync server. Next: Setup or recover on your first device, or Pair this device." |
| Refused URL | pass, with a wording finding (#303) | - | A mistyped address was refused with "Mobile Obsidian only reaches HTTPS servers.". Check then said "type your server's address in Server URL first." while the field still held the refused text. |
| Pair | pass | 25 s | Rig A made the code and it was pasted on the phone; the code left its field once the server held the claim. Both screens showed the same match code, so it was approved. The phone asked "Add this vault's notes to the server's vault? This vault has 1 note…", answered Pair and upload; the notice said "this device is paired, and its first sync is running."; the dialog closed by itself. |
| First sync | pass | < 30 s | 2,088 files came down; Show sync status read idle, Remote only 0. |
| P2 desktop to phone | pass | < 10 s | A note made on rig A was listed and opened on the phone with its text. |
| P3 phone to desktop | pass | < 5 s | A note made on the phone was on rig A with the exact text; no stray Untitled.md. |
| P4 rename on the phone | pass | < 5 s | Rig A had the new name with identical text, the old name gone, no copy. |
| P5 desktop edit, note open on the phone | pass | about 5 s | The edit appeared in the open note without reopening it. |
| P6 delete on the phone | pass | < 5 s | Deleted to the system trash on the phone, and gone from rig A. |
| Show sync status | pass | - | Vault key present, State idle, Files tracked 2088, Remote only 0, Per-file ceiling 512 MiB, Total budget 50 GiB; Recovery phrase "Not confirmed — Show and confirm…"; Recent listed every notice, newest first. |
Windows. A Windows virtual machine with Obsidian desktop, plugin final
(7887d36f), paired with rig A on macOS.
| # | Outcome | Time | Observed |
|---|---|---|---|
| W1 | pass | 1.3 s | A note made on Windows appeared on macOS under the same name with identical bytes (49 bytes). |
| W2 | pass | 1.2 s | A note made on macOS appeared on Windows the same way. |
| W3 | pass | 0.4 s | A folder of 3 notes and a subfolder with 1, renamed on Windows: macOS lists the same 4 files and bytes under the new name, the old name gone, trash unchanged on both. |
| W4 | pass | 0.3 s | A selected folder renamed on Windows: macOS lists its 2 notes under the new name; the Windows selection then holds the new path, not the old. Renamed back afterwards. |
| W5 | pass | 0.5 s each way | A note and a folder renamed by case only, on each side in turn: the other side shows the new spelling on disk, 1 copy, no deletion. |
| W6 | pass | 0.8 s each way | A note deleted on each side in turn left the other; status there "synced", 0 error notices. |
| W7 | pass | at the next sync | A junction inside a selected folder, pointing outside the vault, was refused with "does not sync linked folders: "Linked 83c8" is a link, so it stays on this device only and nothing from your other devices is written into it. To sync it, move the folder itself into the vault instead…"; macOS received 0 files from it. |
| W8 | pass on the rerun | 0.5 s | Restore from history, the first version, Restore a copy: the notice said the copy was saved; 1 restored copy on Windows with the first version's exact bytes, the same copy on macOS, 0 error notices. The first run was graded fail only because the driver still expected 1.1.4's words ("Copy saved locally at"); 1.1.5 says "restored a copy as". |
| W9 | pass | 0.4 s | The same note edited on both sides while Windows was cut off: 1 conflict copy on each side, both edits kept. |
| W10 | pass | 4.6 s | A 64 MiB file of random bytes written on Windows arrived on macOS in 4.6 s; SHA-256 identical. |
On the same VM and build, the #302 series: a note from macOS reached the
Windows device in each of 20 cycles; 4 of them met a lost read, logged
scan decision=stalled call=lstat duration_ms=15588 to 15777
budget_ms=15000, and still arrived, in 13.8 to 14.3 s.
#311 and #310 on the iPhone, final10. Run on 2026-10-01 by the
coordinator, after the review asked for both changes on a real phone. The
user's iPhone was driven through iPhone Mirroring from the Mac. Obsidian
mobile ran a disposable vault (obsync-train-115b), installed from a ZIP
holding only final10's three plugin files (main.js e8562598…, rebuilt from
395e942e) and a "Deleted files" setting of the vault's own .trash.
The server was a fresh obsyncd built from 395e942e. The phone reached it
over HTTPS through a quick tunnel and a logging pass-through, which recorded
every request's device, path and status and the chunk ids it asked for,
without changing them. One desktop with final10 was the setup device and
the peer. Before the phone paired, that desktop made the history:
- 6 notes created, synced, then deleted (answered Delete everywhere);
- one note edited 5 times, each edit synced before the next.
The feed then held 201 live notes and 11 versions with content that were no longer their file's head: the 6 deleted notes' creates and the edited note's versions 0 to 4. A fresh desktop paired over the same route first, as a control, and skipped exactly those 11.
| Journey | Outcome | Observed |
|---|---|---|
| Install, Check | pass | The ZIP was downloaded in Safari, unzipped in Files and opened in Obsidian; Self Hosted Private Sync v1.1.5 enabled. Check: "reached your obsync server." |
| Pair | pass | The user entered the code from the desktop's Pair a new device dialog, and the match codes were compared and approved. The phone read "Paired as iPhone T9MB (iPhone)". |
| #311 first sync over history | pass | The server's log for the phone: 4 chunk batches between 15:31:18.7Z and 15:31:20.3Z, 201 chunk ids asked, all distinct, 543,052 bytes. 0 of the 11 superseded chunk ids were asked, so none of the deleted notes or older edits ever reached the phone. Single chunk reads 0, version reads 0. The control desktop on final10 took the same 201 ids and the same 543,052 bytes. On the phone, the vault listed 202 files: the 201 live notes and the vault's own daily note. The edited note opened at its last version only ("fh 7414 edit v5"). A search for the deleted notes' names found none, and Show sync status read idle, Files tracked 232 after the next row. |
| #310 upload, then a restart | pass | 30 notes of one chunk each (P310 6ce5/, 1,029 bytes each) were moved into the vault in Files. The phone sent 30 chunks and 31 versions (the 30 notes and their folder). The desktop then held 232 files at the same feed sequence, 655. Obsidian was then closed from the app switcher and reopened at 15:35:37Z. In the 5 minutes after: 0 version reads, 0 chunk reads, and 1 chunks/exists question naming 232 chunk ids, all distinct. That is every record on the phone, the 30 it had just sent included, asked about in one question. On final9 a desktop left 473 of 2,000 such notes without one and read them back one a second. |
| Sweep | pass | Show sync status: State idle, Remote only 0, Recent "Nothing since obsync started.", no notice on screen. |
Afterwards the phone was switched back to the user's own vault, untouched, and the test vaults and ZIPs, this run's and the earlier one's, were deleted in Files. They wait in Files' Recently Deleted for the user. The server, tunnels and desktop were torn down.
Findings¶
- The J6 notice no longer says that the server keeps the history. This
was a plan error, not a product one:
docs/validation.mdJ6 now names the 1.1.5 words, in the same commit as this record. - The storage words stood for up to five minutes after the refused file was deleted. Found on final2, filed as #305, fixed in final3. The live row cleared in 1.0 s on final3 (see "Desktop fault journeys").
- A Settings window closed during a first sync lost a disk read, and the device stopped for good. Found on final3 and filed as #307. final4 bounded every desktop disk call, but a start that met the bound still stopped for good, under words promising a retry (1 run of 5). final5 starts again: 0 WEDGED in 10 runs. So did final6 (10 runs), final7 (5 runs) and final9, which awaits a change past the bound (5 runs). See "A Settings window closed while the disk is busy".
- The out-of-storage toast stays after the server has room again, while the status reads synced. Found on the phone on final3, and final4 holds the same code. Filed as #308 and fixed in final5, live on the phone (see its row).
- #307's bound cost a desktop Sync now press about 0.5 s (10 %). Found on final5. final6 replaced the timer per call with one watchdog, and was still 154 ms slower. final7 dropped a second stat per note read, and is inside noise at this sample, with a point estimate of +68 ms. final9 and final10 are inside noise against final8 (+0.4 % and −0.4 %). See "What #307's bound costs".
- Recent ended on a question already answered. After Delete everywhere, the bulk-deletion question stayed the last line in Show sync status's Recent, worded as still waiting. Found in the final7 sweep, filed as #309, and fixed in final8, which says the answer in its own notice (see its row).
- A new device wrote every version the server keeps, and trashed every
note deleted elsewhere (#311). On final9 a fresh device downloaded
each superseded version, wrote it, and moved each deleted note to its
trash: 22 extra writes and 17 notes in its
.trashhere, and 8.75 MB more from the server, growing with the server's history. Found on final9, filed as #311, fixed in final10: 34 of 34 superseded versions skipped, applied equal to live notes, an empty trash. See "A first sync over the server's history". - An uploading desktop missed the sid of about a quarter of its new one-chunk notes, and read each back from the server later, one a second (#310). 473 of 2,000 on final9, and about a third of the 9,800 at setup, which kept rig A reading versions back for 41 minutes. Found on final9, filed as #310, fixed in final10: 0 of 2,000 missed, and 0 version reads in the 5 minutes after a restart. 942 setup records made after A's walk began still had no sid when it ended, and the next walk began reading them back. See "The repair walk after an upload".
What was not validated¶
- A physical Android phone. The phone rows ran on an emulator.
- The final build on Windows and on the Android emulator, and the final build's other journeys on the iPhone. The iPhone ran ffec3fb4 for its journeys, and final10 for #311 and #310 only. Windows ran final. The Android emulator ran every build from ffec3fb4 to final8 except final, and #308, the change that is the phone's own, ran there. final9 ran on the desktops only.
- final9's
overrunandlatelines on a real lost answer: none of its five #307 runs met one. - A Linux desktop.
- Either reference route, a TLS terminator, and a certificate on the Android phone (the iPhone reached the server over HTTPS through a quick tunnel).
- A production-path install, through Community plugins. Every build was copied in by hand.
- The security warning's alert icon (see its row).