Found while investigating #15337 (out of that card's scope; filing rather than fixing there).
What was measured
A live objectstack serve --ui process on this machine, still answering GET /api/v1/health with 200 after 1h02m, holding three deleted file descriptors:
$ ls -l /proc/13209/fd | grep -E '\.db'
22 -> /home/user/hotcrm-issue-1576/.objectstack/data/objectstack.db (deleted)
24 -> /home/user/hotcrm-issue-1576/.objectstack/data/objectstack.db-wal (deleted)
25 -> /home/user/hotcrm-issue-1576/.objectstack/data/objectstack.db-shm (deleted)
$ ls -l /proc/13209/cwd
/home/user/hotcrm-issue-1576 (deleted)
The documented dev loop contains rm -rf .objectstack/data (it is in the repro block of #15337 and in hotcrm's own demo:reset script). Running it while a server still holds the database unlinks the inode without touching the process: SQLite keeps reading and writing the now-invisible file, and a later boot creates a brand-new objectstack.db at the same path.
Why it is worth a boot check
From that moment the two halves of any investigation describe different files, with nothing saying so:
- the running server answers from the unlinked inode;
sqlite3 .objectstack/data/objectstack.db, a table scan, a row edit, or a hash transplant all land on the new file;
- so a row edit made with the server stopped "has no observable effect", and a user that authenticates against the live server "is not in the database".
Both of those readings were reported as evidence in #15337 and are what made it look like the write side and the read side were different stores. The investigation that followed cost a full P0 cycle. Nothing in the product is wrong at that point — the deployment simply has no way to notice, and the failure has the shape AGENTS.md's degradation-level rule names: the system keeps looking healthy while something it claims is true has not landed.
Detection inputs already exist
- The resolved database path is already known at boot (
resolveProjectDatabaseUrl, printed on the Driver: banner row). For a file: URL, one fs.existsSync per check answers "the file I opened is still the file at my path"; comparing the inode (fs.statSync().ino) against the one captured at open is exact.
runtime.ENVIRONMENT.json (written by publishBoundPort under the resolved OS home) already carries pid and port, so "another server already holds this project's database" is answerable at boot without new state.
Shape suggestion, deliberately not a design: refuse nothing, report once — a boot-time line when the resolved database path is already held by a live pid from the runtime state file, and a periodic or on-error check that the opened inode still matches the path, logged at error when it does not (the consequence is that every external observation of this deployment is now false, which is the level that rule reserves).
Not in scope for #15337
#15337's premise is falsified separately (the 401 there came from a NODE_ENV=production objectstack serve boot, where the dev-admin seed is disarmed by design). This card is only about the silent unlinked-database condition, which is version-independent and reproduces by deleting .objectstack/data under any running server.
Generated by Claude Code
Found while investigating #15337 (out of that card's scope; filing rather than fixing there).
What was measured
A live
objectstack serve --uiprocess on this machine, still answeringGET /api/v1/healthwith200after 1h02m, holding three deleted file descriptors:The documented dev loop contains
rm -rf .objectstack/data(it is in the repro block of #15337 and in hotcrm's owndemo:resetscript). Running it while a server still holds the database unlinks the inode without touching the process: SQLite keeps reading and writing the now-invisible file, and a later boot creates a brand-newobjectstack.dbat the same path.Why it is worth a boot check
From that moment the two halves of any investigation describe different files, with nothing saying so:
sqlite3 .objectstack/data/objectstack.db, a table scan, a row edit, or a hash transplant all land on the new file;Both of those readings were reported as evidence in #15337 and are what made it look like the write side and the read side were different stores. The investigation that followed cost a full P0 cycle. Nothing in the product is wrong at that point — the deployment simply has no way to notice, and the failure has the shape AGENTS.md's degradation-level rule names: the system keeps looking healthy while something it claims is true has not landed.
Detection inputs already exist
resolveProjectDatabaseUrl, printed on theDriver:banner row). For afile:URL, onefs.existsSyncper check answers "the file I opened is still the file at my path"; comparing the inode (fs.statSync().ino) against the one captured at open is exact.runtime.ENVIRONMENT.json(written bypublishBoundPortunder the resolved OS home) already carriespidandport, so "another server already holds this project's database" is answerable at boot without new state.Shape suggestion, deliberately not a design: refuse nothing, report once — a boot-time line when the resolved database path is already held by a live pid from the runtime state file, and a periodic or on-error check that the opened inode still matches the path, logged at
errorwhen it does not (the consequence is that every external observation of this deployment is now false, which is the level that rule reserves).Not in scope for #15337
#15337's premise is falsified separately (the 401 there came from a
NODE_ENV=productionobjectstack serveboot, where the dev-admin seed is disarmed by design). This card is only about the silent unlinked-database condition, which is version-independent and reproduces by deleting.objectstack/dataunder any running server.Generated by Claude Code