2026-07-12 00:28:17 +08:00
// Passive-pull indexing orchestration for the Core package.
2026-07-12 00:37:32 +08:00
import { DB_PATH , openDb , openReadDb , openWriterLeaseDb , rebuildMemoryFts } from './db.ts' ;
2026-07-09 09:20:37 +08:00
import {
CLAUDE_DIR , CODEX_DIR , PROJECTS_DIR , fs , path , isDir , readLines ,
2026-07-20 22:43:23 +08:00
inferProjectPath , codexDbId ,
2026-07-12 00:37:32 +08:00
} from './parsing.ts' ;
2026-07-20 22:43:23 +08:00
import {
createProviderIndexPlan ,
indexProviderPlan ,
writeProviderIndexMarkers ,
} from './provider-indexing.ts' ;
2026-07-10 18:10:45 +08:00
import { nodeSqliteTransactionAdapter } from './tx.ts' ;
import { acquireWriterLease , writerLockPathFor } from './writer-lease.ts' ;
import { runRetryableWriteTransaction , isBeginBusyFailure , hasUnusableTransaction } from './write-coordinator.ts' ;
2026-07-20 22:43:23 +08:00
import { createBuiltinProviderRegistry } from './providers/builtins.ts' ;
2026-07-12 00:44:06 +08:00
import type { NodeSqliteDb , SqliteRow } from './sqlite-types.ts' ;
2026-05-31 03:29:41 +08:00
const HISTORY_PATH = path . join ( CLAUDE_DIR , 'history.jsonl' );
2026-07-12 00:37:32 +08:00
type JsonRecord = Record < string , any >;
2026-06-17 23:40:36 +08:00
2026-07-12 00:37:32 +08:00
interface SkippedFile {
path : string ;
error : string ;
diagnostics? : unknown ;
}
interface BuildCheckOptions {
now? : number ;
ignoreRecentBuild? : boolean ;
}
function errorMessage ( error : unknown ) : string {
return error instanceof Error ? error.message : String ( error );
}
2026-07-12 00:44:06 +08:00
function indexCodexSessionIndex ( db : NodeSqliteDb ) : void {
2026-06-17 23:40:36 +08:00
const indexPath = path . join ( CODEX_DIR , 'session_index.jsonl' );
if ( ! fs . existsSync ( indexPath )) return ;
readLines ( indexPath , ( line ) => {
2026-07-12 00:37:32 +08:00
let item : JsonRecord ;
2026-06-17 23:40:36 +08:00
try {
2026-07-10 18:10:45 +08:00
item = JSON . parse ( line );
2026-06-17 23:40:36 +08:00
} catch ( e ) {
2026-07-12 00:37:32 +08:00
process . stderr . write ( `Warning: malformed Codex session index line: ${ errorMessage ( e ) } \ n` );
2026-07-10 18:10:45 +08:00
return ;
2026-06-17 23:40:36 +08:00
}
2026-07-10 18:10:45 +08:00
if ( ! item . id || ! item . thread_name ) return ;
db . prepare ( 'UPDATE sessions SET title=COALESCE(title, ?), ended_at=COALESCE(ended_at, ?) WHERE id=? AND source=?' )
. run ( item . thread_name , item . updated_at || null , codexDbId ( item . id ), 'codex' );
2026-06-17 23:40:36 +08:00
});
}
2026-07-12 00:44:06 +08:00
function refreshSessionProjectPaths ( db : NodeSqliteDb ) : void {
2026-06-10 03:15:32 +08:00
const sessions = db . prepare ( 'SELECT id, project FROM sessions' ). all ();
const cwdStmt = db . prepare ( `
SELECT cwd
FROM messages
WHERE session_id = ? AND cwd IS NOT NULL AND cwd != ''
ORDER BY timestamp IS NULL, timestamp
` );
const update = db . prepare ( 'UPDATE sessions SET project_path = ? WHERE id = ?' );
for ( const session of sessions ) {
2026-07-12 00:44:06 +08:00
const cwds = cwdStmt . all ( session . id ). map (( row : SqliteRow ) => row . cwd );
2026-06-10 03:15:32 +08:00
const projectPath = inferProjectPath ( session . project , cwds );
if ( projectPath ) update . run ( projectPath , session . id );
}
}
2026-07-12 00:44:06 +08:00
function indexWorkflows ( db : NodeSqliteDb ) : void {
2026-05-31 03:29:41 +08:00
if ( ! fs . existsSync ( PROJECTS_DIR )) return ;
let projects ;
try { projects = fs . readdirSync ( PROJECTS_DIR ); } catch { return ; }
for ( const proj of projects ) {
const pp = path . join ( PROJECTS_DIR , proj );
if ( ! isDir ( pp )) continue ;
let entries ;
try { entries = fs . readdirSync ( pp ); } catch { continue ; }
for ( const sd of entries ) {
const wd = path . join ( pp , sd , 'workflows' );
if ( ! isDir ( wd )) continue ;
let wfFiles ;
try { wfFiles = fs . readdirSync ( wd ); } catch { continue ; }
for ( const f of wfFiles ) {
if ( ! f . endsWith ( '.json' )) continue ;
2026-07-12 00:37:32 +08:00
let wf : JsonRecord ;
2026-05-31 03:29:41 +08:00
try {
2026-07-10 18:10:45 +08:00
wf = JSON . parse ( fs . readFileSync ( path . join ( wd , f ), 'utf8' ));
} catch ( e ) {
2026-07-12 00:37:32 +08:00
process . stderr . write ( `Warning: failed to read workflow ${ f } : ${ errorMessage ( e ) } \ n` );
2026-07-10 18:10:45 +08:00
continue ;
}
if ( ! wf . runId ) continue ;
const ac = db . prepare ( 'SELECT COUNT(*) as c FROM workflow_agents WHERE run_id=?' ). get ( wf . runId );
db . prepare ( 'INSERT OR REPLACE INTO workflows (run_id,session_id,task_id,script,result_json,timestamp,agent_count,duration_ms,total_tokens,status,workflow_name) VALUES(?,?,?,?,?,?,?,?,?,?,?)' ). run (
wf . runId , sd , wf . taskId || null , wf . script || null ,
wf . result ? JSON . stringify ( wf . result ) : null , wf . timestamp || null , ac ? . c || 0 ,
wf . durationMs || null , wf . totalTokens || null , wf . status || null , wf . workflowName || null );
const progress = wf . workflowProgress || [];
for ( const item of progress ) {
if ( item . type !== 'workflow_agent' || ! item . agentId ) continue ;
db . prepare ( 'UPDATE workflow_agents SET phase=?, label=?, model=?, state=?, duration_ms=?, tokens=?, tool_calls=? WHERE agent_id=?' ). run (
item . phaseTitle || null , item . label || null , item . model || null , item . state || null ,
item . durationMs || null , item . tokens || null , item . toolCalls || null , 'agent-' + item . agentId );
}
2026-05-31 03:29:41 +08:00
}
}
}
}
2026-07-12 00:44:06 +08:00
function indexHistory ( db : NodeSqliteDb ) : void {
2026-05-31 03:29:41 +08:00
if ( ! fs . existsSync ( HISTORY_PATH )) return ;
readLines ( HISTORY_PATH , ( line ) => {
2026-07-12 00:37:32 +08:00
let item : JsonRecord ;
2026-05-31 03:29:41 +08:00
try {
2026-07-10 18:10:45 +08:00
item = JSON . parse ( line );
} catch ( e ) {
2026-07-12 00:37:32 +08:00
process . stderr . write ( `Warning: malformed history line: ${ errorMessage ( e ) } \ n` );
2026-07-10 18:10:45 +08:00
return ;
}
if ( item . sessionId && item . title ) db . prepare ( 'UPDATE sessions SET title=? WHERE id=? AND title IS NULL' ). run ( item . title , item . sessionId );
2026-05-31 03:29:41 +08:00
});
}
2026-06-04 15:49:57 +08:00
const BUILD_DEBOUNCE_MS = 30000 ;
2026-06-13 03:42:01 +08:00
const APP_HEARTBEAT_FRESH_MS = 60000 ;
2026-07-12 00:44:06 +08:00
function shouldSkipBuild ( db : NodeSqliteDb , { now = Date . now (), ignoreRecentBuild = false } : BuildCheckOptions = {}) {
2026-06-13 03:42:01 +08:00
const appHeartbeat = db . prepare ( "SELECT mtime FROM index_state WHERE jsonl_path='__app_heartbeat__'" ). get ();
2026-07-10 18:10:45 +08:00
if ( appHeartbeat && now - appHeartbeat . mtime < APP_HEARTBEAT_FRESH_MS ) {
return { skip : true , reason : 'daemon_active' };
2026-06-13 03:42:01 +08:00
}
2026-07-10 18:10:45 +08:00
if ( ! ignoreRecentBuild ) {
const last = db . prepare ( "SELECT mtime FROM index_state WHERE jsonl_path='__last_build__'" ). get ();
if ( last && now - last . mtime < BUILD_DEBOUNCE_MS ) {
return { skip : true , reason : 'recent_build' };
}
2026-06-13 03:42:01 +08:00
}
return { skip : false };
}
2026-06-04 15:49:57 +08:00
2026-07-12 00:37:32 +08:00
function isMissingIndexStateTable ( error : unknown ) : boolean {
2026-07-10 18:10:45 +08:00
const message = error instanceof Error ? error.message : String ( error );
return /no such table:\s*(?:main\.)?index_state\b/i . test ( message );
}
2026-07-12 00:37:32 +08:00
function inspectBuildOwnership ({ force = false } : { force? : boolean } = {}) {
2026-07-10 18:10:45 +08:00
if ( ! fs . existsSync ( DB_PATH )) return { skip : false };
const db = openReadDb ();
try {
return shouldSkipBuild ( db , { ignoreRecentBuild : force });
} catch ( error ) {
// A missing table means the write path must initialize a new/legacy index.
// Any other read failure leaves daemon ownership unknown, so fail closed.
if ( isMissingIndexStateTable ( error )) return { skip : false };
throw error ;
} finally {
db . close ();
}
}
2026-07-12 00:37:32 +08:00
function buildIndex ({ force = false } : { force? : boolean } = {}) {
2026-07-10 18:10:45 +08:00
const ownership = inspectBuildOwnership ({ force });
if ( ownership . skip ) return ownership ;
const lease = acquireWriterLease ({
lockPath : writerLockPathFor ( DB_PATH ),
openDb : openWriterLeaseDb ,
});
if ( ! lease ) return { skip : true , reason : 'writer_busy' };
2026-05-31 03:29:41 +08:00
try {
2026-07-10 18:10:45 +08:00
// Ownership may change between the first read and lease acquisition.
const ownershipAfterLease = inspectBuildOwnership ({ force });
if ( ownershipAfterLease . skip ) return ownershipAfterLease ;
const db = openDb ();
const txDb = nodeSqliteTransactionAdapter ( db );
2026-07-12 00:37:32 +08:00
const skippedFiles : SkippedFile [] = [];
2026-07-10 18:10:45 +08:00
try {
try {
if ( force ) {
runRetryableWriteTransaction ( txDb , () => {
db . prepare ( "DELETE FROM index_state WHERE jsonl_path != '__last_build__'" ). run ();
// Clearing index_state alone re-indexes existing files but leaves rows for
// files that no longer exist on disk (stale sessions accumulate). A force
// build is a clean rebuild: drop every derived table, then re-index from the
// current files. `memories` is the durable, human-approved layer and is never
// cleared; messages_fts is repopulated by the 'rebuild' command in finalize.
for ( const table of [ 'messages' , 'tool_calls' , 'tool_results' , 'sessions' , 'summaries' , 'subagents' , 'workflows' , 'workflow_agents' ]) {
db . prepare ( `DELETE FROM ${ table } ` ). run ();
}
}, { label : 'force-cleanup' });
}
} catch ( error ) {
if ( isBeginBusyFailure ( error )) {
return { skip : true , reason : 'database_busy' , skipped : skippedFiles.length , skippedFiles };
}
throw error ;
}
2026-07-20 22:43:23 +08:00
const registry = createBuiltinProviderRegistry ();
const providerPlan = createProviderIndexPlan ( db , registry , { force });
const providerResult = indexProviderPlan ({
db ,
plan : providerPlan ,
runTransaction : ( label , work ) => runRetryableWriteTransaction ( txDb , work , { label }),
onError : ( error , { provider , unit }) => {
if ( isBeginBusyFailure ( error )) return 'stop' ;
if ( hasUnusableTransaction ( error )) throw error ;
const detail = error as { message? : unknown ; obelisk? : unknown } | null ;
const message = errorMessage ( error );
skippedFiles . push ({ path : unit.key , error : message , diagnostics : detail?.obelisk });
process . stderr . write ( `Warning: failed to index ${ provider . name } unit ${ unit . key } : ${ message } \ n` );
return 'skip' ;
},
});
if ( providerResult . stopped ) {
return { skip : true , reason : 'database_busy' , skipped : skippedFiles.length , skippedFiles };
2026-07-10 18:10:45 +08:00
}
// Finalize is one transaction and is NOT swallowed: a finalize failure fails
// the build (a half-finalized index would be inconsistent).
try {
runRetryableWriteTransaction ( txDb , () => {
indexWorkflows ( db );
refreshSessionProjectPaths ( db );
indexHistory ( db );
indexCodexSessionIndex ( db );
db . exec ( "INSERT INTO messages_fts(messages_fts) VALUES('rebuild')" );
rebuildMemoryFts ( db );
db . prepare ( "INSERT OR REPLACE INTO index_state (jsonl_path, mtime, lines_processed) VALUES ('__last_build__', ?, 0)" ). run ( Date . now ());
2026-07-20 22:43:23 +08:00
writeProviderIndexMarkers ( db , providerPlan , providerResult );
2026-07-10 18:10:45 +08:00
}, { label : 'finalize' });
} catch ( error ) {
if ( isBeginBusyFailure ( error )) {
return { skip : true , reason : 'database_busy' , skipped : skippedFiles.length , skippedFiles };
}
throw error ;
}
return { skip : false , skipped : skippedFiles.length , skippedFiles };
} finally {
db . close ();
}
} finally {
lease . release ();
2026-05-31 03:29:41 +08:00
}
}
2026-07-09 09:20:37 +08:00
export { buildIndex , inferProjectPath , refreshSessionProjectPaths , shouldSkipBuild };