@@ -83,7 +83,7 @@ const DB_FILES = ['cortex.db', 'memory.db', 'lens.db', 'vault.db', 'plugins.db']
8383
8484const MAX_BACKUPS = 5 ;
8585
86- async function checkpointWal ( db : Db ) : Promise < void > {
86+ export async function checkpointWal ( db : Db ) : Promise < void > {
8787 try {
8888 await db . run ( 'PRAGMA wal_checkpoint(TRUNCATE)' ) ;
8989 } catch {
@@ -145,29 +145,40 @@ async function pruneBackups(): Promise<void> {
145145async function tryRecover ( dbPath : string ) : Promise < boolean > {
146146 if ( ! await exists ( dbPath ) ) return false ;
147147
148- // Try opening the DB. SQLite auto-recovers WAL from unclean shutdown.
149- // Only treat as corrupt if it throws SQLITE_CORRUPT.
150148 let isCorrupt = false ;
149+ let corruptReason = '' ;
151150 try {
152151 const testClient = createClient ( { url : `file:${ dbPath } ` } ) ;
153- await testClient . execute ( 'SELECT 1' ) ;
152+ const result = await testClient . execute ( 'PRAGMA integrity_check' ) ;
153+ const rows = result . rows as unknown as Array < { integrity_check : string } > ;
154+ for ( const row of rows ) {
155+ if ( row . integrity_check !== 'ok' ) {
156+ isCorrupt = true ;
157+ corruptReason = row . integrity_check ;
158+ break ;
159+ }
160+ }
154161 testClient . close ( ) ;
155162 } catch ( e ) {
163+ const msg = ( e as Error ) . message ?? '' ;
156164 if (
157- ( e as Error ) . message ?. includes ( 'SQLITE_CORRUPT' ) ||
158- ( e as Error ) . message ?. includes ( 'disk image is malformed' )
165+ msg . includes ( 'SQLITE_CORRUPT' ) ||
166+ msg . includes ( 'disk image is malformed' ) ||
167+ msg . includes ( 'malformed database schema' )
159168 ) {
160- console . log ( ` ⚠ ${ basename ( dbPath ) } is corrupted (SQLITE_CORRUPT ) — will recover` ) ;
169+ console . log ( ` ⚠ ${ basename ( dbPath ) } is corrupted (${ msg . slice ( 0 , 80 ) } ) — will recover` ) ;
161170 isCorrupt = true ;
171+ corruptReason = msg ;
162172 } else {
163- // Other errors (permission, etc.) — don't try to recover
164- console . error ( ` ✗ ${ basename ( dbPath ) } open failed: ${ ( e as Error ) . message } ` ) ;
173+ console . error ( ` ✗ ${ basename ( dbPath ) } open failed: ${ msg } ` ) ;
165174 return false ;
166175 }
167176 }
168177
169178 if ( ! isCorrupt ) return false ;
170179
180+ console . log ( ` ⚠ ${ basename ( dbPath ) } integrity failure: ${ corruptReason . slice ( 0 , 120 ) } ` ) ;
181+
171182 const entries : Deno . DirEntry [ ] = [ ] ;
172183 for await ( const entry of Deno . readDir ( PATHS . backupsDir ) ) {
173184 if ( entry . isDirectory ) entries . push ( entry ) ;
@@ -199,18 +210,80 @@ async function tryRecover(dbPath: string): Promise<boolean> {
199210 }
200211 }
201212
202- return true ;
213+ // Verify restored backup passes integrity check
214+ try {
215+ const verifyClient = createClient ( { url : `file:${ dbPath } ` } ) ;
216+ const verifyResult = await verifyClient . execute ( 'PRAGMA integrity_check' ) ;
217+ const verifyRows = verifyResult . rows as unknown as Array < { integrity_check : string } > ;
218+ const verifyOk = verifyRows . length === 1 && verifyRows [ 0 ] . integrity_check === 'ok' ;
219+ verifyClient . close ( ) ;
220+ if ( verifyOk ) {
221+ console . log ( ` ✓ ${ basename ( dbPath ) } restored from backup — integrity check passed` ) ;
222+ return true ;
223+ } else {
224+ console . warn (
225+ ` ⚠ Restored ${ basename ( dbPath ) } still has integrity issues — trying older backup` ,
226+ ) ;
227+ }
228+ } catch {
229+ console . warn (
230+ ` ⚠ Restored ${ basename ( dbPath ) } failed to open — trying older backup` ,
231+ ) ;
232+ }
203233 }
204234
205235 if ( isCorrupt ) {
206236 console . error (
207- ` ✗ ${ basename ( dbPath ) } is corrupted and no backup exists — manual recovery required` ,
237+ ` ✗ ${ basename ( dbPath ) } has corruption and no healthy backup found.` ,
238+ ) ;
239+ console . error (
240+ ` To recover manually:` ,
241+ ) ;
242+ if ( await exists ( PATHS . backupsDir ) ) {
243+ console . error ( ` 1. Find a healthy backup in ${ PATHS . backupsDir } ` ) ;
244+ console . error ( ` 2. Copy cortex.db from the backup into ${ PATHS . dataDir } ` ) ;
245+ console . error ( ` 3. Remove any <dbname>-wal and <dbname>-shm files` ) ;
246+ console . error ( ` 4. Restart the server` ) ;
247+ }
248+ console . error (
249+ ` If no healthy backup exists, delete the corrupted database to start fresh.` ,
208250 ) ;
209251 }
210252
211253 return false ;
212254}
213255
256+ async function integrityCheck ( db : Db , label : string ) : Promise < string [ ] > {
257+ try {
258+ const rows = await db . all < { integrity_check : string } > ( 'PRAGMA integrity_check' ) ;
259+ const errors : string [ ] = [ ] ;
260+ for ( const row of rows ) {
261+ const val = row . integrity_check ;
262+ if ( val !== 'ok' ) errors . push ( val ) ;
263+ }
264+ return errors ;
265+ } catch ( e ) {
266+ return [ `${ label } : integrity_check failed — ${ ( e as Error ) . message } ` ] ;
267+ }
268+ }
269+
270+ async function checkAllDatabases (
271+ dbs : Map < string , Db > ,
272+ labels : Map < string , string > ,
273+ ) : Promise < boolean > {
274+ let allOk = true ;
275+ for ( const [ name , db ] of dbs ) {
276+ const label = labels . get ( name ) ?? name ;
277+ const errors = await integrityCheck ( db , label ) ;
278+ if ( errors . length > 0 ) {
279+ allOk = false ;
280+ console . error ( ` ✗ ${ label } integrity errors:` ) ;
281+ for ( const err of errors ) console . error ( ` • ${ err } ` ) ;
282+ }
283+ }
284+ return allOk ;
285+ }
286+
214287export async function runMigrations ( ) : Promise < void > {
215288 await ensureDir ( PATHS . dataDir ) ;
216289 await ensureDir ( PATHS . sessionsDir ) ;
@@ -230,6 +303,34 @@ export async function runMigrations(): Promise<void> {
230303 const vaultDb = await getVaultDb ( ) ;
231304 const pluginsDb = await getPluginsDb ( ) ;
232305
306+ const integrityLabels = new Map ( [
307+ [ 'cortex.db' , 'cortex.db' ] ,
308+ [ 'memory.db' , 'memory.db' ] ,
309+ [ 'lens.db' , 'lens.db' ] ,
310+ [ 'vault.db' , 'vault.db' ] ,
311+ [ 'plugins.db' , 'plugins.db' ] ,
312+ ] ) ;
313+ const integrityDbs = new Map < string , Db > ( [
314+ [ 'cortex.db' , coreDb ] ,
315+ [ 'memory.db' , memoryDb ] ,
316+ [ 'lens.db' , lensDb ] ,
317+ [ 'vault.db' , vaultDb ] ,
318+ [ 'plugins.db' , pluginsDb ] ,
319+ ] ) ;
320+
321+ if ( ! await checkAllDatabases ( integrityDbs , integrityLabels ) ) {
322+ console . error (
323+ '\n Database corruption detected. The server cannot start with corrupted databases.' ,
324+ ) ;
325+ console . error (
326+ ' To recover: restore from backups in ' + PATHS . backupsDir ,
327+ ) ;
328+ console . error (
329+ ' Backups are created automatically before each migration run.' ,
330+ ) ;
331+ Deno . exit ( 1 ) ;
332+ }
333+
233334 const dbMap = new Map < string , Db > ( [
234335 [ 'cortex.db' , coreDb ] ,
235336 [ 'memory.db' , memoryDb ] ,
@@ -443,6 +544,11 @@ export async function runMigrations(): Promise<void> {
443544 sqlFile : '055_mcp_gateway_approvals.sql' ,
444545 label : 'cortex.db (MCP gateway approvals)' ,
445546 } ,
547+ {
548+ db : coreDb ,
549+ sqlFile : '056_workspace_policy.sql' ,
550+ label : 'cortex.db (workspace boundary policy)' ,
551+ } ,
446552 ] ;
447553
448554 for ( const { db, sqlFile, label } of targets ) {
@@ -471,6 +577,14 @@ export async function runMigrations(): Promise<void> {
471577 // Run sensitivity backfill if needed (one-time after adding sensitivity columns)
472578 const { runBackfill } = await import ( '../security/backfill.ts' ) ;
473579 await runBackfill ( ) ;
580+
581+ // Final health check — warn (don't abort) on post-migration issues
582+ const postCheckOk = await checkAllDatabases ( integrityDbs , integrityLabels ) ;
583+ if ( ! postCheckOk ) {
584+ console . warn (
585+ ' ⚠ Post-migration integrity issues detected. Some features may be degraded.' ,
586+ ) ;
587+ }
474588}
475589
476590export async function createAutoAdmin (
0 commit comments