Plainva 0.6.8: one account, however often you connect it
The last release fixed the anchor in a task note. This one fixes the thing that threw the anchor away.
Reconnecting is how you repair a login
An expired sign-in is not an exotic state. Tokens run out, a provider revokes a grant, you change a password — and the obvious repair is to connect the account again. Until now that produced a second account sitting next to the first, and the task reconcile treated it as a stranger: every mirrored task got created a second time.
The row that matters is pim_task_state. It is the only thing that links a task note to its task at the provider. Lose it and the reconcile no longer recognises the note, creates the task again, and stops reporting its deletion — exactly the danger the previous release had already named out loud.
So the rows are reassigned rather than discarded, and on a collision the target wins: its row carries the history, while the fresh one has at most half a cycle behind it. Adoption happens only on an identity the provider has confirmed — not on a display name. Two people at one company easily share a name, and the settings sync refuses to merge on a label for precisely that reason. With CalDAV, where there is no confirmed identity, a separate entry appears as before. That is a limitation I would rather state than paper over.
Two orders that carry the whole thing
Reading the code turned up something I had not expected: validating the connection already renews the token, and Microsoft rotates it while doing so. The rotated token lands under the throwaway id, because that is the id the auth provider was built with. Write the caller’s credentials under the target id and the surviving account is handed a spent token.
Two sequences hold this together, and each is pinned by its own failing counter-check: the rows move before the throwaway account row is deleted (calendars and task lists cascade off it), and the token is written before the throwaway slot is cleared.
I also learned something about the checking itself. One counter-check stayed green when I removed only the early identity guard — the protection sits in two places, and the neighbouring assertion caught it. Both together measure the behaviour rather than one particular guard, and the test now says so next to itself.
Where a note came from
Google published OKF 0.2 in July. The bump is additive — a 0.1 bundle drops in unchanged — and it brings five optional families that answer one question: how much do you trust this document?
A note can now carry where it came from (generated, sources), who checked it (verified), whether it is a draft or deprecated (status), and when it goes stale (stale_after). Plainva stamps them only where a machine writes: import, mail capture, and the task reconcile when it creates a note. The editor never touches a trust family, and nothing existing gets stamped after the fact. Your notes are yours; a tool that retroactively claims authorship of them would be lying.
The single trust action a human takes is Mark as reviewed. It appends human:<name> to the list rather than replacing it — the list is the review history, and a second reviewer never overwrites the first.
The rule without which this would have done damage is a shape check before the claim: a task note with its own status: Open column keeps its ordinary property and gets no lifecycle badge. Without it, every task database in the world would have grown lifecycle semantics overnight.
Two things are deliberately not built. log.md belongs to the AI harness, not here — today it would have no reader, mechanical entries would miss the spec’s prose intent, and a per-save log would be the most conflict-prone file in the vault. And trust is display, not enforcement: a stale document is marked, never locked.
Four things the phone was missing
Vault-wide find and replace. Folder overviews. The importer, with 27 sources including Notion. And OKF conversion.
The order was not arbitrary — the data-safety requirement rises from the first to the last. Find and replace writes to many notes, but each on its own. Folder overviews write and rename, so links have to come along. The importer writes a whole vault and touches a foreign secret for the first time (the Notion token goes to the credential store, never into the vault and never into settings sync). And conversion writes into every note — while a phone can take its power away at any moment.
So the conversion run was lifted, not rebuilt. Backup per file, verification after writing, skip-with-report, cancel, dry run — all of that already existed on the desktop, and the unchanged desktop tests are the proof that nothing moved while lifting it. On top comes only what a phone actually needs: a journal written before the first change (a file, atomic, outside the container so no half state travels to other devices; without a journal, nothing is converted at all), a recovery on next start that asks instead of acting, and a preview that names the notes.
The open question in my plan answered itself while building: the backup folder is already the register. The run copies each file immediately before overwriting it, so the folder is the complete list of what was touched. Resuming writes into the same folder — two folders would mean two undos, neither of which restores the vault on its own.
“Later” is a real answer, asked once per app start. A sheet you cannot get rid of on your own phone is worse than the state it warns about. And the desktop got the same recovery in this release: a desktop process is not killed mid-run, but a power cut is exactly that case and leaves exactly that half-converted vault.
A 90 MB file, edited every two minutes
Plainva keeps a snapshot of every write, so you can go back. For a large attachment that arithmetic gets ugly fast: ninety megabytes, changed every two minutes, could leave a hundred copies in .plainva — nine gigabytes for one file.
Above 5 MB a write now keeps one snapshot instead of the full history, decided on the file size before reading it, so those ninety megabytes never enter memory. Only writes, and that is the rule itself: a write leaves the new version on disk, so history there is comfort. A deletion is the case where the snapshot is the only thing that brings the file back, and it is still backed up in full.
There is deliberately no slider. Anyone who sets it to zero gets the nine gigabytes back.
The first external code contribution
KDOJP found something specific and real: on macOS, a folder name with combining characters is indexed decomposed (ü as u + ¨), while a .base typed on a keyboard holds it composed. SQLite compares byte for byte — the database rendered and stayed empty. Every Mac vault with accented folder names is affected, not just the Japanese one that surfaced it.
What convinced me was the level he chose. Path identity in Plainva is deliberately not made normalisation-insensitive, because that would re-key sync_state and the index of every existing vault. He did not touch the index; he asked for both forms at query time.
The debt was one layer down, and I took it in the same release: the in-memory evaluator — which takes over for nested filter groups — still compared byte for byte, so a repaired database would have gone empty again the moment someone added a filter group. And the test now measures the behaviour rather than the shape of the SQL: real SQLite, a note under a decomposed folder, queried from a composed source.
iOS asks for 16.4
The bundle has required Safari 16.4 since the shared packages started using lookbehind. The Xcode project still declared 15.0 — so the App Store offered the app to devices where it could only ever stay white. Raising the floor costs nothing that ever ran.
And the phone could not even say so: its fatal overlay lived inside the module graph, dead in exactly the case it was written for. It has the desktop’s guard now. The two platforms need different sentences, though: on iOS the engine ships with the system, so a version number is the only true statement; on Android the WebView updates separately, so the honest answer is an action rather than a number.
Everything is on GitHub. Existing installations update themselves. If you hit something, tell me — the last release shipped a fix that started as somebody’s throwaway half-sentence.