Skip to content

Quilltap 4.8.3

Choose a tag to compare

@csebold csebold released this 13 Aug 22:11
· 2529 commits to main since this release

Quilltap 4.8.3 Release Notes

Two corrections, both of them concerning staff who reported success while accomplishing nothing whatever.

There is a particular species of failure that is worse than a loud one, and both of this release's occupants belong to it. A component that breaks noisily is a component that will be fixed. A component that breaks and then files a cheerful report is a component that will be trusted for as long as nobody thinks to check — which, in the case of the second of these, was rather longer than one would like.

The Locksmith Who Locked Himself In

On a genuinely fresh instance — a first run, a new copy, the very first minutes anyone spends with the software — completing the encryption-key setup left the house in an interesting condition. The front door still answered. The setup screen said the work was done, and it was: the databases were converted correctly and not one byte was lost. But every subsequent request to the cellar came back with the database connection is not open, and went on doing so until the process was stopped and started again.

The setup handler had closed the SQLite client before converting the files, which is entirely sensible and respects exactly one of the three parties who had an opinion on the matter. The backend went on cheerfully offering the closed handle to anyone who asked; the manager went on offering the backend. Neither had been told anything. So a first-time user, at the precise moment they had the least accumulated reason to trust the software, met a wall of errors — recoverable in full by a restart nobody had told them to perform.

Teardown now goes through two new manager functions, suspendDatabase() and resumeDatabase(), which close and reopen every handle while keeping the backend instance itself alive. That last part matters more than it sounds: the backend carries the column maps that tell structured data how to become itself again, and a rebuilt backend would hand every live repository an empty set of them, whereupon JSON and arrays and booleans would quietly start round-tripping as raw strings — a cure decidedly worse than the disease.

Four further defects came up in the same excavation, all of them relatives. The LLM-logs connection stayed open on its file while that file was replaced, so subsequent log writes went into an inode that no longer had a name. The mount-index database was not converted at setup at all, which left document-store bytes sitting in plaintext on disk until the next restart. disconnect() closed two of the three databases connect() opens. And auto-lock had precisely the same illness, so locking and unlocking wedged the house in exactly the same way, without even the courtesy of a restart notice. All four are fixed; lock now suspends, unlock resumes, and no restart is required.

Should the reopen fail anyway, the response still carries your one-time encryption key. It is displayed exactly once in this life and will not be withheld behind an error message.

The Sentry Who Had Been Asleep Since Tuesday

Quilltap keeps a guard on the cellar stairs whose sole duty is to stop an older copy of the software from opening a database that a newer copy has already migrated. It is a small guard with one job, and it had not been doing that job since the twelfth of August — while reporting, at every boot, that all was well.

The mechanism is unglamorous. The guard reached into the migration utilities with a synchronous require(). Those utilities had become an asynchronous module in the bundler's graph the moment an earlier fix added an import to them, and a synchronous require() of an asynchronous module hands back an exports object whose body has never run: every property undefined, every call a TypeError, straight into a catch that permitted startup regardless. The only evidence in the world was two error lines at the top of every boot log, saying isSQLiteBackend is not a function, in a log nobody reads when everything appears fine.

The consequences were quiet and entirely structural. The highest-version marker was never written to the database — instances created since that date have no such row at all — the minimum-server-version never reached the key file where the desktop shell looks for it, and an older binary would have opened a newer database without a word of complaint. Nothing was corrupted by the bug itself. The guard was simply not there.

Both guard functions are now async and use await import(...), which is how every other part of the application reaches into the migration layer. The import edge that caused this was deliberately left standing: unwinding it would have restored the guard today and left the next innocent static import free to break it again, silently, on some future Tuesday.

Three further precautions were taken, on the theory that a guard who cannot report his own absence is not much of a guard. The failure now surfaces as a warning through the migration-warnings channel, so it reaches a human rather than expiring in a log file. A lint rule fails the build on any synchronous require() of the migration layer from application code. And the tests now assert the effect — that the version is actually written, that a failed guard actually announces itself — rather than merely that nothing threw, which is the assertion that let this through in the first place.

No repair is needed for existing instances. The correct value is written on the next boot: an instance with no row gets one, and an instance frozen at an old version is corrected upward.


What Changed

  • fix (Saquel Ytzama): Completing first-run encryption setup no longer wedges every database connection until restart. Teardown goes through new suspendDatabase() / resumeDatabase() manager functions that close and reopen all handles while preserving the cached backend instance and its column maps. Also in the same pass: the LLM-logs client is now closed before its file is converted (writes were landing in the unlinked pre-conversion inode), the mount-index database is converted at setup instead of being left in plaintext until the next restart, SQLiteBackend.disconnect() closes all three databases rather than two, and auto-lock uses suspend/resume so the lock → unlock cycle no longer wedges. The backend self-heals a handle closed behind its back, except while suspended. If the reopen fails, the one-time encryption key is still returned and the setup screen shows a restart notice. (bug 64)
  • fix (Foundry): The startup version guard, inert since 2026-08-12, works again. lib/startup/version-guard.ts used a synchronous require() of migrations/lib/database-utils, which had become an async module in the bundler's graph — every property undefined, every call a TypeError swallowed by a catch that allowed startup anyway. Consequently instance_settings.highest_app_version was never written, minServerVersion never reached .dbkey, and an older binary would open a newer database without complaint. Both guard functions are now async with await import(...), and instrumentation.ts awaits them. Failures now raise a migration warning instead of dying at error level in the boot log, an ESLint no-restricted-syntax rule fails the build on require() of migrations/ from lib/, app/, components/, or hooks/, and the tests assert the written value rather than the absence of a throw. No repair needed — the correct value is written on the next boot. (bug 65)
  • internal (Foundry): The better-sqlite3 test mock now throws TypeError: The database connection is not open from prepare/exec/pragma after close(), matching the real driver. A mock that kept answering after close is what hid bug 64's entire class of defect.

Installation

Desktop App

Download from the quilltap-shell releases page:

macOS:

  1. Download the .dmg file and open it
  2. Drag Quilltap to your Applications folder
  3. Launch Quilltap from Applications

Windows:

  1. Download and run the .exe installer
  2. If SmartScreen warns about an unknown publisher, click "More info" → "Run anyway"
  3. Launch Quilltap from the Start Menu or desktop shortcut

Linux:

  1. Download the .AppImage file, make it executable (chmod +x), and run it
  2. Or install the .deb package: sudo dpkg -i quilltap_*.deb

Node.js (any platform)

npx quilltap

Or install globally:

npm install -g quilltap
quilltap

Open http://localhost:3000 in your browser. Requires Node.js 24+. First run downloads ~150–250 MB and caches locally.

Docker

docker pull foundry9/quilltap:4.8.3

Or use the startup scripts:

# Linux / macOS
curl -fsSL https://raw.githubusercontent.com/foundry-9/quilltap-server/refs/heads/main/scripts/start-quilltap.sh | bash

# Windows (PowerShell)
irm https://raw.githubusercontent.com/foundry-9/quilltap-server/refs/heads/main/scripts/start-quilltap.ps1 | iex

A patch release about two kinds of silence. The first was the silence of a locksmith who had shut the door behind himself and could not hear anyone knocking; the second, the silence of a sentry who had been reporting an empty corridor from somewhere else entirely.

Both posts are now manned, and both, more usefully, will say so if they are ever abandoned again.

Installation

Desktop App (recommended)

The Quilltap desktop app (Electron) is available from
quilltap-shell 4.1.12.
Download the release for your platform (macOS, Windows, or Linux).

The quilltap-linux-arm64.tar.gz and quilltap-linux-amd64.tar.gz rootfs
tarballs attached to this release are used by the shell's Lima (macOS) and WSL2 (Windows) VM modes.

Node.js (any platform)

npm install -g quilltap
quilltap

On first run, the CLI downloads the application files (~150-250 MB)
and caches them locally. Subsequent launches start instantly.

Docker

docker pull foundry9/quilltap:4.8.3

See the README for setup instructions.