mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 00:02:39 +00:00
* 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>
94 lines
3.7 KiB
TypeScript
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'
|
|
}
|