Repository navigation
Releases
basaltd 0.9.1
basaltd 0.9.1
Upgrade if you run basaltd 0.9.0 or earlier. This release fixes four server defects, one of which could send a note to a device you had just revoked. Your devices need no update: protocol 7 and the data format are unchanged, and every Basalt client from 0.8.0 on, including the 0.9.0 plugin, keeps working exactly as it does against 0.9.0.
What is fixed
A revoked device could still be sent a note. When you revoked a device, a note another device saved in the moment between the revoke taking effect and the revoked device's connection closing could still be delivered to it, and a Basalt device holds the key to read what it is sent. The same was true of a device unlinking itself, and of a revoked device still catching up on a backlog, which could read notes saved after the revoke. The revoked device's connections now leave delivery in the same step that removes the device, and the only thing they hear afterwards is the notice that they were revoked. The revoke itself, and every other device, behave as before.
A batched commit could still fail with disk I/O error (6410). 0.8.5 told SQLite to keep its temporary files in memory, which the read-only container needs because it has no temporary directory, but the setting only ever reached one of the server's database connections. Every connection now gets it, including the read-only ones verify, stats and backup inspection use. The one-entry-at-a-time fallback added in 0.8.5 stays, so even before this fix such a failure slowed a vault rather than stopping it.
The name of the vault this server serves was visible to anyone who could reach the port. A connection naming any other vault was refused with a message naming the one served, and a malformed pairing attempt was answered differently for the served vault than for any other. Both now get the same refusal a wrong credential gets. The server's log still records which vault was asked for and which is served.
Invalid text in a request was silently changed. A request carrying invalid UTF-8, or half of a UTF-16 surrogate pair, had those characters replaced with U+FFFD, so a device name, for example, was stored as something other than what was sent, and the device was told it had worked. Such a request is now refused and the connection closed. No Basalt client sends one.
What you might notice
A connection that names the wrong vault, for example from a mistyped setup line, is now told "not authorised for this vault", like any other credential failure, rather than which vault the server serves. The server log says both.
Upgrading
Use ghcr.io/waynehoover/basalt-sync:0.9.1, preferably pinned to its published digest, or the matching binary below. Stop the previous server cleanly and keep the existing data directory, ownership and configuration: nothing in the data directory changes, no migration runs, and no device needs re-pairing.
Verification and limits
Every fix has a regression test that fails on 0.9.0 and passes here, and the release checks include the server's race tests, a backup and restore rehearsal, protocol integration tests and execution of both published container architectures. Process termination tests do not simulate power loss. Existing local-filesystem and trusted-device assumptions remain; network filesystems are unsupported.
Verify the downloaded files and their provenance:
shasum -a 256 -c SHA256SUMS
gh attestation verify basaltd-linux-amd64 --repo waynehoover/basalt-syncBasalt Sync 0.10.0
MCP grew the tools it was missing, and learned to serve more than one vault.
Nothing in the sync engine or the server moved.
Several vaults from one endpoint
basalt mcp --vault personal=/srv/personal --vault work=/srv/work exposes up to
ten separately paired directories from one process. Each keeps its own sync
connection, keys, history, read queue and mutation queue; the process takes
every lock before it starts and holds them until all admitted work drains.
list_vaults reports names, access modes and readiness without leaking host
paths. With more than one configured, every other tool requires a vault
argument, because a shared "current vault" setting is a way to write into the
wrong one.
One caveat, stated plainly: the HTTP credential belongs to the first configured
directory and authorizes the whole endpoint in its launch mode. If different
clients should reach different vaults, run separate endpoints with separate
credentials.
Tags, and the preview that comes before them
add_tags, remove_tags, manage_tags and rename_tag, plus a tag mode for
search_notes that matches nested descendants and reads frontmatter and body
while ignoring code, comments and link syntax.
Every tag and namespace tool returns a bounded preview first. Applying it
requires handing back every affected base and the exact edits; if recomputing
the plan produces anything different, the request is refused. A batch validates
all of its inputs and verifies and flushes every before-image before it
publishes a single change, then rechecks each source and reports per-file
results. A failure part-way keeps the completed work and all recovery copies
rather than rolling back over somebody else's save.
Also
prepend_note, which inserts exact text at the start and keeps an existing
UTF-8 BOM where it belongs.compare_versions, for the difference between two historical versions, or
between one and what is on disk now.delivery_status, which says whether your other devices have actually
received what you wrote: received, waiting, or unconfirmed.create_directory.- Moves publish a verified destination, update approved links, and delete the
source last and recoverably. They use ordinary sync create/delete semantics
and do not promise atomic remote visibility or history across the rename.
Permanent deletion and mutation of recovery copies are still unavailable, and
there is still no whole-file writer. A correct base proves nobody else changed
the note; it does not prove a model's replacement kept everything.
Upgrading
Update the plugin through Obsidian or replace its three files. CLI users can run
npm install -g basalt-sync@0.10.0. Server 0.9.0 is unchanged and needs no
upgrade: no server file moved in this release, protocol 7 and the stored data
format are the same, and existing pairings keep working.
Basalt Sync 0.9.0
What's new
- The headless CLI now exposes a separately paired vault through MCP, over local stdio or Streamable HTTP behind Tailscale Serve or another HTTPS proxy.
- Agents can search and read notes, inspect sync and history, create notes, append text, make exact old-to-new edits, and restore a version to a new path. HTTP MCP is read-only by default;
--writableenables supported mutations. Stdio follows the paired device's read-only setting. - Existing-note edits require the current content base and a verified, flushed before-image before changing the original. Whole-file replacement is intentionally absent. Backups remain ordinary synced files that MCP cannot change.
- HTTP access uses an independent, revocable bearer key. Rotation retires old sessions without changing the vault's device or encryption credentials.
- Obsidian setup now accepts an invite, recovery key or server setup line in one field. Device naming moves under More options, and the unnamed default vault is no longer presented as a user-chosen name.
Upgrade
Update the Obsidian plugin normally, preserving its settings and pairing. It still requires Obsidian 1.7.2 or newer. The matching CLI is available with npm install -g basalt-sync@0.9.0 and now requires Node.js 22 or newer.
All 0.8.x and 0.9.0 components use protocol 7. Existing pairings and encrypted server data remain compatible; there is no re-pairing or data migration. The server 0.9.0 release accompanies this release but has no runtime changes from 0.8.5.
For MCP, follow the CLI setup guide. Pair a separate local directory; never run the headless client against the same directory as a running Obsidian plugin. Keep HTTP on loopback behind an HTTPS proxy, and export its key outside the vault.
Verification and limits
The local gate covers real server exchanges, packaged CLI execution, concurrent edits, crash recovery, backup retention and HTTP authentication. Real Tailscale HTTPS, credential rotation and restart were also exercised with official SDK clients; Mac and Android devices running plugin 0.8.6 received the exact edited note and its original before-image.
MCP remains experimental. Cloudflare Tunnel, Collie and a specific phone MCP app have not been tested. Static bearer authentication is supported; OAuth discovery is not. A lost response can follow a completed edit, so inspect the note before retrying. Process-kill tests do not establish power-loss durability, and Android retains its existing filesystem flush limitations.
The release includes checksums and build provenance:
shasum -a 256 -c SHA256SUMS
gh attestation verify main.js --repo waynehoover/basalt-syncbasaltd 0.9.0
What changed
This server release accompanies the Basalt 0.9.0 CLI and Obsidian plugin release. The server runtime is unchanged from 0.8.5. Its startup test now waits for the server to be serving before checking contention, avoiding a false failure between its two lock acquisitions.
MCP runs in the separately paired headless client, not in basaltd. The sync server continues to store encrypted content and does not receive the client's plaintext notes or decryption keys.
Upgrade
Protocol 7 and the on-disk format are unchanged. Existing 0.8.x clients remain compatible, and existing data and pairings are retained. No migration or re-pairing is required.
Use ghcr.io/waynehoover/basalt-sync:0.9.0, preferably pinned to its published digest, or the matching binary below. Stop the previous server cleanly and keep the existing data directory, ownership and configuration. The release provides Linux amd64/arm64 container images and Linux/macOS amd64/arm64 binaries.
There is no new server feature needed to enable MCP. Install the 0.9.0 CLI on a trusted host, pair a separate local vault, and follow the MCP guide.
Verification and limits
Release checks include server race tests, a backup/restore rehearsal, protocol integration tests and execution of both published container architectures. Process termination tests do not simulate power loss. Existing local-filesystem and trusted-device assumptions remain; network filesystems are unsupported.
Verify the downloaded files and their provenance:
shasum -a 256 -c SHA256SUMS
gh attestation verify basaltd-linux-amd64 --repo waynehoover/basalt-syncBasalt Sync 0.8.6
One fix, from watching my own vault stop syncing for a day.
A server that answers is not a server that works
Basalt's server 0.8.4 had a bug that made it refuse every batch a device sent.
Server 0.8.5
fixes that, and you want it.
This release is about what the client did while that was happening. It
connected, was refused, and came back three seconds later. Twenty two thousand
times in a day. The backoff that exists to prevent exactly this, and which
climbs to five minutes, never got past its first step, because it reset the
moment the handshake finished.
Connecting is not progress. The reset now happens after the device has
reconciled against the server, which is the smallest thing that means the
session was worth having. A connection that drops after that is a network, and
the next attempt is quick, as before. A connection that achieves nothing and
repeats now backs off.
Nothing else about reconnection changes. A refusal your device cannot apply
still stops with an explanation after three identical failures, and an ordinary
dropped connection is still retried promptly.
Upgrading
Update the plugin through Obsidian or replace its three files. CLI users can
run npm install -g basalt-sync@0.8.6. Protocol 7 and the stored data format
are unchanged, so this works against a server on 0.8.4 or 0.8.5 and existing
pairings keep working. Upgrade the server too if you have not.
Basalt Sync 0.8.5
Faster on a large vault, and a first sync that no longer opens an empty box at you.
Faster
- Renaming a folder no longer re-encrypts everything inside it. A rename changes no byte, so the server already has every chunk; the plugin was reading, splitting and encrypting every note under the folder to rediscover a list it already had. At 10,000 notes that move is 1,177 ms → 832 ms.
- Listing the vault is about twice as fast on a large one. 37.9 ms → 21.1 ms at 50,000 notes. Every path was building a throwaway list and sorting it to answer a question that is false for essentially every path, and filenames were being Unicode-normalised again on every pass.
- Less allocation in an ordinary pass, and the open note is published between the pieces of a large download rather than after it.
Clearer
- A first sync with nothing to do says so, instead of offering a "File details" chevron that opens onto an empty box. It also tells "everything here already matches" apart from "there is nothing here yet".
- The connection details say the one thing worth acting on, in one line, rather than three sentences about encryption in a panel whose last row links to the guide.
Upgrade
Update the plugin through Obsidian or replace its three files. CLI users can run npm install -g basalt-sync@0.8.5. Take server 0.8.5 as well. When this note first went out it said the server was unchanged and needed no upgrade. Within the hour I found 0.8.4 refusing every batch on my own server, so there is now a server 0.8.5 that fixes it, and you want it. Protocol 7 and the stored data format are still the same, so a client on either version works against a server on either version, and existing pairings keep working.
The timings above are on a laptop, measured with bun run bench:pass and the new BENCH_LIST=1 listing benchmark; docs/research.md has the method and the first Android figures this project has taken.
basaltd 0.8.5
Upgrade if you are running 0.8.4. This fixes a bug that can stop a vault
syncing entirely.
A batch that cannot commit still commits
0.8.4 started committing a device's batch as one transaction: one fsync for a
hundred notes instead of a hundred. That is a real speedup and it is the right
default. What it did not have was anything to fall back on.
On my own server, a day after upgrading, every batch a device sent was being
refused with disk I/O error (6410). The device connected, offered its work,
was refused, the session ended, and it tried again three seconds later, twenty
two thousand times in a day. Nothing was lost, because nothing was
acknowledged. Nothing synced either.
0.8.5 falls back to committing one entry at a time whenever the batch cannot
commit. That is what every release before 0.8.4 did: it costs an fsync per
entry and it works. A speedup that can stop a vault syncing is worse than the
fsyncs it saves.
And a likely cause of that error
6410 is SQLite asking where it may put a temporary file. The published image
is built FROM scratch and has no /tmp in it, and a SAVEPOINT, which the
batched commit uses, is what makes SQLite want a statement journal in one. So
0.8.5 also sets temp_store = MEMORY, which keeps that scratch space in the
process.
When I wrote this I could not reproduce the failure, and said the pragma went
in as the correct setting rather than as a diagnosis. Deploying it changed
that. The server that had refused 461 batches in its last half hour on 0.8.4
committed the device's waiting work within seconds of starting on 0.8.5, and
in the five minutes after: zero errors, one stable session instead of a
reconnect every four seconds, and the fallback never fired. The batch
committed as a batch, on the one code path the pragma touches.
That is strong evidence and still short of proof, because the container was
also recreated and I never reproduced the failure in a controlled setting.
What would settle it is a 0.8.4 image with only the pragma added, on that
host. The fallback stays in regardless: it is what makes the next unreproducible
store failure a slow vault instead of a stopped one.
Upgrading
The protocol is unchanged, so clients on 0.8.4 and 0.8.5 both work against
this. Pull the new image and restart; nothing in your data directory changes.
Basalt Sync 0.8.4
A note whose upload acknowledgement was lost could stop syncing from that device for good. This release fixes that, cuts what a download costs when a large file changes a little, and gives the panel a way to say what is actually outstanding.
The stall
If the connection dropped in the moment between the server committing your write and the acknowledgement arriving, that device never learned the version it had just written. Every later upload of that path was refused as out of date, including every edit you made afterwards, while the panel said it was waiting. A refusal now asks the server which version it holds, so the device can catch up on the one fact the ordinary update stream never carries.
Faster
- Editing a large file no longer re-downloads all of it. Other devices now rebuild the unchanged parts from the copy they already hold and fetch only what changed.
- A note saved during a big download goes out immediately instead of waiting for the download to finish.
- Send back what the server has lost offers the whole vault at once rather than one request per file.
- Less memory while assembling a download, and less reading and encrypting for previews, folder-deletion reviews and moved attachments.
Clearer
- Pending work no longer shows a finished tick, and the reasons a file did not sync stay in the panel instead of disappearing with a notice.
- Pairing now names the vault and server an invite would join, before you press the button.
- Skip on this device: leave a folder off one device, choosable during pairing so a phone never downloads what it was never going to keep.
- Browse deleted has a search box, keeps the pages you have loaded, and restores a selected set in one sync instead of one per note.
- Renaming a note no longer hides the history it had under its old name.
- Versions kept somewhere Obsidian does not show now have a button that recovers a visible copy.
- On a phone, "some files need attention" and "offline" arrive as a notice rather than only in a tooltip.
Bases and named vaults
.basefiles are treated as notes: sent in pieces, merged rather than conflicted, and previewable in history. Merged results are checked as YAML first.- A setup line can name its vault (
host:3003#TOKEN#vault), so the plugin can start one that is not calleddefaultwithout needing the CLI on another machine.
Upgrade
Update the plugin through Obsidian or replace its three plugin files. CLI users can run npm install -g basalt-sync@0.8.4. Server 0.8.4 is published separately; upgrade it first. Protocol 7 and the stored data format are unchanged, and existing pairings keep working.
A setup line that names its vault needs a plugin or CLI from 0.8.4: older ones read everything after the last # as the token.
The download and repair figures above are read off the code and confirmed by tests, not measured on a phone. What is deliberately still undone, including the memory ceiling on very large attachments, is written down in docs/open-work.md.
basaltd 0.8.4
Basalt server 0.8.4 cuts the disk waiting out of a batch of writes.
- A batch of entries now commits in one transaction with a savepoint per entry, so it costs one fsync instead of one per entry while each entry keeps its own
staleorbadentryanswer. A folder rename or a bulk delete carries no file bodies, so those fsyncs were the whole cost of it. - Chunk directories are flushed concurrently after their bodies land, with every flush completing before anything is acknowledged. A batch of 256 chunk names touches most of the 256 shard directories, and those flushes ran one after another.
- A setup line for a server started with
-vault NAMEnow carries the name (host:3003#TOKEN#NAME), so the Obsidian plugin can claim a vault that is not calleddefault. A line with no second#meansdefault, exactly as before. - Protocol 7 and the database format are unchanged. Existing devices stay paired.
Update to ghcr.io/waynehoover/basalt-sync:0.8.4 or use the attached binary for your platform. Back up the data directory before upgrading. Continue to expose the server through Tailscale Serve or another TLS reverse proxy.
Clients from 0.8.3 and earlier read everything after the last # as the token, so a named setup line needs a client from 0.8.4.
Basalt Sync 0.8.3
Saved note edits can now sync while a large attachment is uploading. This release also makes sync feedback more responsive and reduces work during large-vault CLI passes.
Plugin
- Keep independent edits to existing text notes moving during large uploads.
- Retry repaired files immediately with Sync now, and wake automatically when transient retries are due.
- Refresh setup after pairing, show activity sooner, and share delivery checks across open panels.
- Preserve keyboard focus when loading more history, add accessible pairing labels, and keep progress icons steady.
- Show loading feedback while preview prepares and provide Resume sync when paused, including while a pause finishes.
CLI and server
- Reuse healthy watcher listings and reduce index comparison work. A local 10,000-note edited pass measured about 51 ms versus 119 ms with a full listing.
- Keep syncing unrelated notes after changing ignored folders.
- Verify shared server chunks once per check and reuse encoded notifications across connections.
Upgrade
Update the plugin through Obsidian or replace its three plugin files. CLI users can run npm install -g basalt-sync@0.8.3. Server 0.8.3 is published separately. Protocol 7 and the stored data format are unchanged; existing pairing continues to work.
The upload improvement applies to independent existing text notes up to 512 KiB. Conflicts, renames, downloads, and attachment preparation still follow the normal sync sequence. The timing above is a local desktop measurement, not a mobile latency promise.