Files
orca/src/main/sqlite/sqlite-read-failure.ts
T
OrcaWinandm4air 8c2cd7d331 feat(ai-vault): read remote OpenCode history with the pinned Node; remove Bun (#24128)
* feat(ai-vault): read OpenCode history with the pinned Node instead of Bun

SSH hosts whose Node lacks node:sqlite (or its backup(), which 22.13-22.15
omit) now get the pinned Node in the shared ~/.orca-remote/runtimes/node-<sha>
store orcad uses: POSIX hosts receive the official archive and extract and
hash-verify it on the host; Windows hosts receive the verified node.exe the
client extracted, promoted by host Node with the same hash check. WSL distros
use the same layout and checks under ~/.cache/orca/runtimes/.

The Bun release pin table and its materializer are deleted. Old relays keep
reading their vault-sqlite/<sha>/bun references; nothing deletes those files.
An unconfirmed runtime upload now keeps its stage instead of removing it.

* refactor(sqlite): drop the Bun SQLite adapter; node:sqlite is the only backend

Nothing outside Electron runs on Bun any more (design D4), so SyncDatabase
loses its Bun branch, and bun-sqlite-database, bun-sqlite-statement and
bun-readonly-wal go, with the relay's bun:sqlite external. The profile-state
backup worker admits Electron or an entry that exists, and startup errors
name the pinned Node. The D7 cross-runtime gate still runs Bun 1.4.2, now
reaching Bun's SQLite through its node:sqlite.

* test(native-chat): drop the Bun SQLite driver case now that node:sqlite is the only backend

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-10-01 01:10:31 -07:00

94 lines
3.7 KiB
TypeScript

// Why (#15036): a read-only SQLite open can fail for reasons that demand
// opposite responses, and both arrive as an opaque driver string. Contention is
// transient — retry later, never persist it as a permanent failure. A missing
// wal-index is structural — retrying forever cannot fix it.
/** Primary result codes; extended codes pack the primary code in the low byte. */
const SQLITE_BUSY = 5
const SQLITE_LOCKED = 6
const SQLITE_CORRUPT = 11
const SQLITE_CANTOPEN = 14
const SQLITE_NOTADB = 26
// Shared with the Codex index-heal pass, which only ever sees a relayed message
// string (app-server RPC drops `errcode`), so message matching is not optional.
const CONTENTION_MESSAGE = /SQLITE_(?:BUSY|LOCKED)|database (?:is )?(?:busy|locked)/i
function primaryErrcode(error: unknown): number | null {
if (!error || typeof error !== 'object') {
return null
}
const errcode = 'errcode' in error ? error.errcode : undefined
return typeof errcode === 'number' && Number.isFinite(errcode) ? errcode & 0xff : null
}
/** node:sqlite marks its errors `ERR_SQLITE_ERROR`. */
function isSqliteDriverError(error: unknown): boolean {
return error instanceof Error && 'code' in error && error.code === 'ERR_SQLITE_ERROR'
}
/** True only when SQLite itself reports the database damaged or not a database at all. */
export function isSqliteCorruption(error: unknown): boolean {
if (!isSqliteDriverError(error)) {
return false
}
const errcode = primaryErrcode(error)
return errcode === SQLITE_CORRUPT || errcode === SQLITE_NOTADB
}
function errorText(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
/**
* True when a SQLite failure means "someone else holds the database right now".
* @param error - The thrown value from a `node:sqlite` call or a relayed message.
* @returns Whether the caller should retry later rather than record a failure.
*/
export function isTransientSqliteContention(error: unknown): boolean {
const errcode = primaryErrcode(error)
if (errcode === SQLITE_BUSY || errcode === SQLITE_LOCKED) {
return true
}
return CONTENTION_MESSAGE.test(errorText(error))
}
export type SqliteReadFailureKind =
/** Another connection holds the lock; the same read can succeed later. */
| 'contended'
/**
* The database file itself is present, but SQLite still could not open the
* set of files it needs — in practice the `-wal`/`-shm` pair a read-only WAL
* open requires, which shares that cannot host SQLite shared memory (9p
* `\\wsl.localhost`, SMB, SSHFS) refuse. Retrying cannot fix it.
*/
| 'wal-index-unavailable'
/** Anything else: corrupt file, wrong schema, permissions. */
| 'unreadable'
/**
* Classify why a read-only SQLite open or query failed.
*
* Pure: the caller supplies the "does the database file exist" evidence, so
* neither branch needs a filesystem probe of its own. That matters on a WSL 9p
* share, where an extra sync stat is exactly the hang the transcript gate exists
* to prevent.
* @param args.error - The thrown value.
* @param args.databaseFileExists - Whether the main database file was present.
* @returns Which of the three failure modes the error represents.
*/
export function classifySqliteReadFailure(args: {
error: unknown
databaseFileExists: boolean
}): SqliteReadFailureKind {
if (isTransientSqliteContention(args.error)) {
return 'contended'
}
// SQLITE_CANTOPEN against a database that is right there names a companion
// file — the wal-index a read-only WAL open needs — not the database itself.
if (primaryErrcode(args.error) === SQLITE_CANTOPEN && args.databaseFileExists) {
return 'wal-index-unavailable'
}
return 'unreadable'
}