2026-07-08 16:11:12 +08:00
import { test } from 'node:test' ;
import assert from 'node:assert/strict' ;
import { createRequire } from 'node:module' ;
import { appendFileSync , mkdtempSync , mkdirSync , statSync , utimesSync , writeFileSync } from 'node:fs' ;
import { tmpdir } from 'node:os' ;
import { join } from 'node:path' ;
const require = createRequire ( import . meta . url );
2026-07-09 10:20:18 +08:00
const { buildIndex } = require ( '../app/src/main/indexer.js' );
2026-07-08 16:11:12 +08:00
const { DatabaseSync } = require ( 'node:sqlite' );
class TestDatabase {
constructor ( dbPath ) {
this . db = new DatabaseSync ( dbPath );
}
pragma ( statement ) {
this . db . exec ( `PRAGMA ${ statement } ` );
}
exec ( sql ) {
return this . db . exec ( sql );
}
prepare ( sql ) {
return this . db . prepare ( sql );
}
close () {
return this . db . close ();
}
}
test ( 'app indexer builds the Obelisk database from Claude JSONL and records app heartbeat' , () => {
const home = mkdtempSync ( join ( tmpdir (), 'obelisk-app-indexer-' ));
const claudeDir = join ( home , '.claude' );
const projectDir = join ( claudeDir , 'projects' , '-tmp-obelisk-app' );
mkdirSync ( projectDir , { recursive : true });
const sessionId = 'session-app-1' ;
const jsonlPath = join ( projectDir , ` ${ sessionId } .jsonl` );
writeFileSync ( jsonlPath , [
JSON . stringify ({
uuid : 'msg-app-1' ,
type : 'user' ,
timestamp : '2026-06-13T10:00:00Z' ,
cwd : '/tmp/obelisk-app' ,
message : { role : 'user' , content : [{ type : 'text' , text : 'hello from app indexer' }] },
}),
'' ,
]. join ( '\n' ));
const dbPath = join ( claudeDir , 'obelisk.sqlite' );
const firstBuild = buildIndex ({ claudeDir , dbPath , DatabaseImpl : TestDatabase });
const db = new TestDatabase ( dbPath );
assert . equal ( db . prepare ( 'SELECT text FROM messages WHERE uuid=?' ). get ( 'msg-app-1' ). text , 'hello from app indexer' );
assert . deepEqual ( firstBuild . affectedSessionIds , [ sessionId ]);
assert . equal ( firstBuild . ftsRebuilt , true );
assert . equal ( db . prepare ( "SELECT uuid FROM messages_fts WHERE messages_fts MATCH 'hello'" ). get (). uuid , 'msg-app-1' );
assert . equal ( db . prepare ( "SELECT jsonl_path FROM index_state WHERE jsonl_path='__app_heartbeat__'" ). get (). jsonl_path , '__app_heartbeat__' );
assert . equal ( db . prepare ( "SELECT jsonl_path FROM index_state WHERE jsonl_path='__app_last_successful_build__'" ). get (). jsonl_path , '__app_last_successful_build__' );
assert . equal ( db . prepare ( 'SELECT project_path FROM sessions WHERE id=?' ). get ( sessionId ). project_path , '/tmp/obelisk-app' );
db . close ();
appendFileSync ( jsonlPath , [
JSON . stringify ({
uuid : 'msg-app-2' ,
type : 'assistant' ,
timestamp : '2026-06-13T10:01:00Z' ,
message : { role : 'assistant' , content : [{ type : 'text' , text : 'companion view updated quickly' }] },
}),
'' ,
]. join ( '\n' ));
const nextMtime = new Date ( Date . now () + 2000 );
utimesSync ( jsonlPath , nextMtime , nextMtime );
const secondBuild = buildIndex ({
claudeDir ,
dbPath ,
DatabaseImpl : TestDatabase ,
changedPaths : [ `-tmp-obelisk-app/ ${ sessionId } .jsonl` ],
});
assert . deepEqual ( secondBuild . affectedSessionIds , [ sessionId ]);
assert . equal ( secondBuild . ftsRebuilt , false );
const db2 = new TestDatabase ( dbPath );
assert . equal ( db2 . prepare ( "SELECT uuid FROM messages_fts WHERE messages_fts MATCH 'companion'" ). get (). uuid , 'msg-app-2' );
assert . equal ( db2 . prepare ( 'SELECT message_count FROM sessions WHERE id=?' ). get ( sessionId ). message_count , 2 );
db2 . close ();
});
test ( 'force rebuild ignores stale JSONL index_state rows after session tables were cleared' , () => {
const home = mkdtempSync ( join ( tmpdir (), 'obelisk-app-indexer-force-' ));
const claudeDir = join ( home , '.claude' );
const projectDir = join ( claudeDir , 'projects' , '-tmp-obelisk-app' );
mkdirSync ( projectDir , { recursive : true });
const sessionId = 'session-force-1' ;
const jsonlPath = join ( projectDir , ` ${ sessionId } .jsonl` );
writeFileSync ( jsonlPath , [
JSON . stringify ({
uuid : 'msg-force-1' ,
type : 'user' ,
timestamp : '2026-06-13T10:00:00Z' ,
cwd : '/tmp/obelisk-app' ,
message : { role : 'user' , content : [{ type : 'text' , text : 'force rebuild should not trust stale state' }] },
}),
'' ,
]. join ( '\n' ));
const dbPath = join ( claudeDir , 'obelisk.sqlite' );
buildIndex ({ claudeDir , dbPath , DatabaseImpl : TestDatabase });
const broken = new TestDatabase ( dbPath );
assert . equal ( broken . prepare ( 'SELECT COUNT(*) AS c FROM index_state WHERE jsonl_path = ?' ). get ( jsonlPath ). c , 1 );
broken . prepare ( 'DELETE FROM messages' ). run ();
broken . prepare ( 'DELETE FROM sessions' ). run ();
broken . close ();
buildIndex ({ claudeDir , dbPath , DatabaseImpl : TestDatabase , force : true });
const db = new TestDatabase ( dbPath );
assert . equal ( db . prepare ( 'SELECT COUNT(*) AS c FROM sessions' ). get (). c , 1 );
assert . equal ( db . prepare ( 'SELECT text FROM messages WHERE uuid=?' ). get ( 'msg-force-1' ). text , 'force rebuild should not trust stale state' );
db . close ();
});
test ( 'force rebuild bypasses stale message FTS delete triggers' , () => {
const home = mkdtempSync ( join ( tmpdir (), 'obelisk-app-indexer-force-fts-' ));
const claudeDir = join ( home , '.claude' );
const projectDir = join ( claudeDir , 'projects' , '-tmp-obelisk-app' );
mkdirSync ( projectDir , { recursive : true });
const sessionId = 'session-force-fts-1' ;
const jsonlPath = join ( projectDir , ` ${ sessionId } .jsonl` );
writeFileSync ( jsonlPath , [
JSON . stringify ({
uuid : 'msg-force-fts-1' ,
type : 'user' ,
timestamp : '2026-06-13T10:00:00Z' ,
cwd : '/tmp/obelisk-app' ,
message : { role : 'user' , content : [{ type : 'text' , text : 'force rebuild should bulk clear without old fts deletes' }] },
}),
'' ,
]. join ( '\n' ));
const dbPath = join ( claudeDir , 'obelisk.sqlite' );
buildIndex ({ claudeDir , dbPath , DatabaseImpl : TestDatabase });
const trapped = new TestDatabase ( dbPath );
trapped . exec ( 'DROP TRIGGER messages_fts_ad' );
trapped . exec ( `
CREATE TRIGGER messages_fts_ad AFTER DELETE ON messages BEGIN
SELECT RAISE(FAIL, 'message FTS delete trigger fired during force rebuild');
END;
` );
trapped . close ();
assert . doesNotThrow (() => {
buildIndex ({ claudeDir , dbPath , DatabaseImpl : TestDatabase , force : true });
});
const db = new TestDatabase ( dbPath );
assert . equal ( db . prepare ( 'SELECT text FROM messages WHERE uuid=?' ). get ( 'msg-force-fts-1' ). text , 'force rebuild should bulk clear without old fts deletes' );
assert . equal ( db . prepare ( "SELECT uuid FROM messages_fts WHERE messages_fts MATCH 'bulk'" ). get (). uuid , 'msg-force-fts-1' );
const trigger = db . prepare ( "SELECT sql FROM sqlite_master WHERE type='trigger' AND name='messages_fts_ad'" ). get ();
assert . match ( trigger . sql , /INSERT INTO messages_fts/ );
db . close ();
});
test ( 'force rebuild into a new database preserves existing memories' , () => {
const home = mkdtempSync ( join ( tmpdir (), 'obelisk-app-indexer-force-preserve-' ));
const claudeDir = join ( home , '.claude' );
const projectDir = join ( claudeDir , 'projects' , '-tmp-obelisk-app' );
mkdirSync ( projectDir , { recursive : true });
const sessionId = 'session-force-preserve-1' ;
writeFileSync ( join ( projectDir , ` ${ sessionId } .jsonl` ), [
JSON . stringify ({
uuid : 'msg-force-preserve-1' ,
type : 'user' ,
timestamp : '2026-06-13T10:00:00Z' ,
cwd : '/tmp/obelisk-app' ,
message : { role : 'user' , content : [{ type : 'text' , text : 'force rebuild should preserve memories' }] },
}),
'' ,
]. join ( '\n' ));
const dbPath = join ( home , '.obelisk' , 'obelisk.sqlite' );
const tempDbPath = join ( home , '.obelisk' , 'obelisk.rebuild.tmp' );
buildIndex ({ claudeDir , dbPath , DatabaseImpl : TestDatabase });
const sourceDb = new TestDatabase ( dbPath );
sourceDb . prepare ( `
INSERT INTO memories (id,session_id,project,message_start,message_end,path,anchors,summary,created_at,deleted_at,deleted_reason)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
` ). run (
'mem-preserve-1' ,
sessionId ,
'-tmp-obelisk-app' ,
'msg-force-preserve-1' ,
'msg-force-preserve-1' ,
'/tmp/obelisk-app/notes.md' ,
'[]' ,
'Decision: preserve memories when rebuilding the session index.' ,
'2026-06-13T10:05:00Z' ,
null ,
null ,
);
sourceDb . close ();
buildIndex ({
claudeDir ,
dbPath : tempDbPath ,
DatabaseImpl : TestDatabase ,
force : true ,
preserveDbPath : dbPath ,
});
const db = new TestDatabase ( tempDbPath );
const memory = db . prepare ( 'SELECT id, summary FROM memories WHERE id=?' ). get ( 'mem-preserve-1' );
assert . equal ( memory . summary , 'Decision: preserve memories when rebuilding the session index.' );
assert . equal ( db . prepare ( "SELECT id FROM memories_fts WHERE memories_fts MATCH 'preserve'" ). get (). id , 'mem-preserve-1' );
assert . equal ( db . prepare ( 'SELECT text FROM messages WHERE uuid=?' ). get ( 'msg-force-preserve-1' ). text , 'force rebuild should preserve memories' );
db . close ();
});
test ( 'app indexer reports changed workflow JSON as an affected session' , () => {
const home = mkdtempSync ( join ( tmpdir (), 'obelisk-app-indexer-workflow-' ));
const claudeDir = join ( home , '.claude' );
const project = '-tmp-obelisk-app' ;
const sessionId = 'session-workflow-1' ;
const projectDir = join ( claudeDir , 'projects' , project );
const workflowDir = join ( projectDir , sessionId , 'workflows' );
mkdirSync ( workflowDir , { recursive : true });
const jsonlPath = join ( projectDir , ` ${ sessionId } .jsonl` );
writeFileSync ( jsonlPath , [
JSON . stringify ({
uuid : 'msg-workflow-1' ,
type : 'user' ,
timestamp : '2026-06-13T10:00:00Z' ,
cwd : '/tmp/obelisk-app' ,
message : { role : 'user' , content : [{ type : 'text' , text : 'start workflow' }] },
}),
'' ,
]. join ( '\n' ));
const dbPath = join ( claudeDir , 'obelisk.sqlite' );
buildIndex ({ claudeDir , dbPath , DatabaseImpl : TestDatabase });
const workflowPath = join ( workflowDir , 'run-1.json' );
writeFileSync ( workflowPath , JSON . stringify ({
runId : 'run-1' ,
timestamp : '2026-06-13T10:01:00Z' ,
workflowName : 'review' ,
}));
const result = buildIndex ({
claudeDir ,
dbPath ,
DatabaseImpl : TestDatabase ,
changedPaths : [ ` ${ project } / ${ sessionId } /workflows/run-1.json` ],
});
assert . deepEqual ( result . affectedSessionIds , [ sessionId ]);
});
test ( 'app indexer marks UI-fallback control messages as meta at ingest time' , () => {
const home = mkdtempSync ( join ( tmpdir (), 'obelisk-app-indexer-meta-' ));
const claudeDir = join ( home , '.claude' );
const projectDir = join ( claudeDir , 'projects' , '-tmp-obelisk-app' );
mkdirSync ( projectDir , { recursive : true });
const sessionId = 'session-meta-1' ;
const jsonlPath = join ( projectDir , ` ${ sessionId } .jsonl` );
writeFileSync ( jsonlPath , [
JSON . stringify ({
uuid : 'msg-meta-system-reminder' ,
type : 'user' ,
timestamp : '2026-06-13T10:00:00Z' ,
message : { role : 'user' , content : [{ type : 'text' , text : '<system-reminder>Keep answers concise</system-reminder>' }] },
}),
JSON . stringify ({
uuid : 'msg-meta-local-command' ,
type : 'user' ,
timestamp : '2026-06-13T10:01:00Z' ,
message : { role : 'user' , content : [{ type : 'text' , text : '<local-command>git status</local-command>' }] },
}),
JSON . stringify ({
uuid : 'msg-meta-normal' ,
type : 'user' ,
timestamp : '2026-06-13T10:02:00Z' ,
message : { role : 'user' , content : [{ type : 'text' , text : 'normal user request' }] },
}),
'' ,
]. join ( '\n' ));
const dbPath = join ( claudeDir , 'obelisk.sqlite' );
buildIndex ({ claudeDir , dbPath , DatabaseImpl : TestDatabase });
const db = new TestDatabase ( dbPath );
assert . equal ( db . prepare ( 'SELECT is_meta FROM messages WHERE uuid=?' ). get ( 'msg-meta-system-reminder' ). is_meta , 1 );
assert . equal ( db . prepare ( 'SELECT is_meta FROM messages WHERE uuid=?' ). get ( 'msg-meta-local-command' ). is_meta , 1 );
assert . equal ( db . prepare ( 'SELECT is_meta FROM messages WHERE uuid=?' ). get ( 'msg-meta-normal' ). is_meta , 0 );
db . close ();
});
test ( 'app indexer loads Codex root sessions into the shared schema' , () => {
const home = mkdtempSync ( join ( tmpdir (), 'obelisk-app-indexer-codex-' ));
const claudeDir = join ( home , '.claude' );
const codexDir = join ( home , '.codex' );
const codexSessionDir = join ( codexDir , 'sessions' , '2026' , '06' , '15' );
mkdirSync ( join ( claudeDir , 'projects' ), { recursive : true });
mkdirSync ( codexSessionDir , { recursive : true });
const codexId = '019ec6ee-cebd-7431-9c93-ceec89a98a5f' ;
const jsonlPath = join ( codexSessionDir , `rollout-2026-06-15T00-19-59- ${ codexId } .jsonl` );
writeFileSync ( jsonlPath , [
JSON . stringify ({
timestamp : '2026-06-14T16:19:59.842Z' ,
type : 'session_meta' ,
payload : {
id : codexId ,
timestamp : '2026-06-14T16:19:59.842Z' ,
cwd : '/tmp/obelisk-app' ,
cli_version : '0.135.0-alpha.1' ,
originator : 'Codex Desktop' ,
source : 'vscode' ,
thread_source : 'user' ,
git : { branch : 'feat/codex' },
},
}),
JSON . stringify ({
timestamp : '2026-06-14T16:20:00.000Z' ,
type : 'event_msg' ,
payload : { type : 'user_message' , message : 'codex user asks for indexing' , images : [], local_images : [], text_elements : [] },
}),
JSON . stringify ({
timestamp : '2026-06-14T16:20:01.000Z' ,
type : 'event_msg' ,
payload : { type : 'agent_message' , phase : 'final_answer' , message : 'codex assistant replies' },
}),
JSON . stringify ({
timestamp : '2026-06-14T16:20:02.000Z' ,
type : 'response_item' ,
payload : { type : 'function_call' , call_id : 'call_codex_1' , name : 'exec_command' , arguments : '{"cmd":"pwd"}' },
}),
JSON . stringify ({
timestamp : '2026-06-14T16:20:03.000Z' ,
type : 'response_item' ,
payload : { type : 'function_call_output' , call_id : 'call_codex_1' , output : '/tmp/obelisk-app' },
}),
JSON . stringify ({
timestamp : '2026-06-14T16:20:04.000Z' ,
type : 'event_msg' ,
payload : {
type : 'task_complete' ,
completed_at : 1781454004 ,
duration_ms : 4321 ,
turn_id : 'turn-1' ,
last_agent_message : 'codex assistant replies' ,
},
}),
'' ,
]. join ( '\n' ));
const dbPath = join ( home , '.obelisk' , 'obelisk.sqlite' );
const result = buildIndex ({ claudeDir , codexDir , dbPath , DatabaseImpl : TestDatabase });
const db = new TestDatabase ( dbPath );
const session = db . prepare ( 'SELECT * FROM sessions WHERE id=?' ). get ( `codex: ${ codexId } ` );
assert . equal ( session . source , 'codex' );
assert . equal ( session . project , '-tmp-obelisk-app' );
assert . equal ( session . project_path , '/tmp/obelisk-app' );
assert . equal ( session . git_branch , 'feat/codex' );
assert . equal ( session . version , '0.135.0-alpha.1' );
assert . equal ( session . message_count , 3 );
assert . deepEqual ( result . affectedSessionIds , [ `codex: ${ codexId } ` ]);
const messages = db . prepare ( 'SELECT role, text, source, content_type, turn_duration_ms FROM messages WHERE session_id=? ORDER BY timestamp, uuid' ). all ( `codex: ${ codexId } ` );
assert . deepEqual ( messages . map ( m => [ m . role , m . text , m . source , m . content_type ]), [
[ 'user' , 'codex user asks for indexing' , 'codex' , 'text' ],
[ 'assistant' , 'codex assistant replies' , 'codex' , 'text' ],
[ 'assistant' , null , 'codex' , 'tool_use' ],
]);
assert . equal ( messages [ 1 ]. turn_duration_ms , 4321 );
const tool = db . prepare ( 'SELECT * FROM tool_calls WHERE id=?' ). get ( 'codex:call_codex_1' );
assert . equal ( tool . session_id , `codex: ${ codexId } ` );
assert . equal ( tool . name , 'exec_command' );
assert . equal ( tool . message_uuid , `codex: ${ codexId } :000004` );
const toolResult = db . prepare ( 'SELECT message_uuid, content FROM tool_results WHERE tool_use_id=?' ). get ( 'codex:call_codex_1' );
assert . equal ( toolResult . message_uuid , `codex: ${ codexId } :000004` );
assert . equal ( toolResult . content , '/tmp/obelisk-app' );
db . close ();
});
test ( 'app indexer accepts Codex changed paths relative to the sessions directory' , () => {
const home = mkdtempSync ( join ( tmpdir (), 'obelisk-app-indexer-codex-sessions-change-' ));
const claudeDir = join ( home , '.claude' );
const codexDir = join ( home , '.codex' );
const codexSessionDir = join ( codexDir , 'sessions' , '2026' , '06' , '15' );
mkdirSync ( join ( claudeDir , 'projects' ), { recursive : true });
mkdirSync ( codexSessionDir , { recursive : true });
const codexId = '019ec6ee-cebd-7431-9c93-ceec89a98a5f' ;
const filename = `rollout-2026-06-15T00-19-59- ${ codexId } .jsonl` ;
writeFileSync ( join ( codexSessionDir , filename ), [
JSON . stringify ({
timestamp : '2026-06-14T16:19:59.842Z' ,
type : 'session_meta' ,
payload : {
id : codexId ,
timestamp : '2026-06-14T16:19:59.842Z' ,
cwd : '/tmp/obelisk-app' ,
cli_version : '0.135.0-alpha.1' ,
thread_source : 'user' ,
},
}),
JSON . stringify ({
timestamp : '2026-06-14T16:20:00.000Z' ,
type : 'event_msg' ,
payload : { type : 'user_message' , message : 'sessions-relative codex change' , images : [], local_images : [], text_elements : [] },
}),
'' ,
]. join ( '\n' ));
const dbPath = join ( home , '.obelisk' , 'obelisk.sqlite' );
const result = buildIndex ({
claudeDir ,
codexDir ,
dbPath ,
DatabaseImpl : TestDatabase ,
changedPaths : [ join ( '2026' , '06' , '15' , filename )],
});
const db = new TestDatabase ( dbPath );
assert . deepEqual ( result . affectedSessionIds , [ `codex: ${ codexId } ` ]);
assert . equal (
db . prepare ( 'SELECT text FROM messages WHERE session_id=?' ). get ( `codex: ${ codexId } ` ). text ,
'sessions-relative codex change' ,
);
db . close ();
});
test ( 'app indexer uses Codex response_item messages only when no visible event message exists' , () => {
const home = mkdtempSync ( join ( tmpdir (), 'obelisk-app-indexer-codex-response-message-' ));
const claudeDir = join ( home , '.claude' );
const codexDir = join ( home , '.codex' );
const codexSessionDir = join ( codexDir , 'sessions' , '2026' , '06' , '15' );
mkdirSync ( join ( claudeDir , 'projects' ), { recursive : true });
mkdirSync ( codexSessionDir , { recursive : true });
const codexId = '019ec6ee-cebd-7431-9c93-ceec89a98a5f' ;
writeFileSync ( join ( codexSessionDir , `rollout-2026-06-15T00-19-59- ${ codexId } .jsonl` ), [
JSON . stringify ({
timestamp : '2026-06-14T16:19:59.842Z' ,
type : 'session_meta' ,
payload : {
id : codexId ,
timestamp : '2026-06-14T16:19:59.842Z' ,
cwd : '/tmp/obelisk-app' ,
cli_version : '0.135.0-alpha.1' ,
source : 'vscode' ,
},
}),
JSON . stringify ({
timestamp : '2026-06-14T16:20:00.000Z' ,
type : 'response_item' ,
payload : {
type : 'message' ,
role : 'developer' ,
content : [{ type : 'input_text' , text : 'developer context should not be indexed' }],
},
}),
JSON . stringify ({
timestamp : '2026-06-14T16:20:01.000Z' ,
type : 'response_item' ,
payload : {
type : 'message' ,
role : 'assistant' ,
phase : 'final_answer' ,
content : [{ type : 'output_text' , text : 'assistant fallback message' }],
},
}),
JSON . stringify ({
timestamp : '2026-06-14T16:20:02.000Z' ,
type : 'event_msg' ,
payload : { type : 'agent_message' , phase : 'final_answer' , message : 'assistant event message' },
}),
JSON . stringify ({
timestamp : '2026-06-14T16:20:03.000Z' ,
type : 'response_item' ,
payload : {
type : 'message' ,
role : 'assistant' ,
phase : 'final_answer' ,
content : [{ type : 'output_text' , text : 'assistant event message' }],
},
}),
'' ,
]. join ( '\n' ));
const dbPath = join ( home , '.obelisk' , 'obelisk.sqlite' );
buildIndex ({ claudeDir , codexDir , dbPath , DatabaseImpl : TestDatabase });
const db = new TestDatabase ( dbPath );
const rows = db . prepare ( 'SELECT text FROM messages WHERE session_id=? ORDER BY timestamp, uuid' ). all ( `codex: ${ codexId } ` );
assert . deepEqual ( rows . map ( row => row . text ), [
'assistant fallback message' ,
'assistant event message' ,
]);
db . close ();
});
test ( 'app indexer skips Codex guardian review threads' , () => {
const home = mkdtempSync ( join ( tmpdir (), 'obelisk-app-indexer-codex-guardian-' ));
const claudeDir = join ( home , '.claude' );
const codexDir = join ( home , '.codex' );
const codexSessionDir = join ( codexDir , 'sessions' , '2026' , '06' , '15' );
mkdirSync ( join ( claudeDir , 'projects' ), { recursive : true });
mkdirSync ( codexSessionDir , { recursive : true });
const guardianId = '019ed5c4-8d52-7bc0-91f3-447a15e987d1' ;
writeFileSync ( join ( codexSessionDir , `rollout-2026-06-15T02-12-00- ${ guardianId } .jsonl` ), [
JSON . stringify ({
timestamp : '2026-06-14T18:12:00.000Z' ,
type : 'session_meta' ,
payload : {
id : guardianId ,
timestamp : '2026-06-14T18:12:00.000Z' ,
cwd : '/tmp/obelisk-app' ,
cli_version : '0.135.0-alpha.1' ,
thread_source : 'subagent' ,
source : { subagent : { other : 'guardian' } },
},
}),
JSON . stringify ({
timestamp : '2026-06-14T18:12:01.000Z' ,
type : 'turn_context' ,
payload : { cwd : '/tmp/obelisk-app' , model : 'codex-auto-review' },
}),
JSON . stringify ({
timestamp : '2026-06-14T18:12:02.000Z' ,
type : 'event_msg' ,
payload : { type : 'user_message' , message : 'approval guardian prompt' , images : [], local_images : [], text_elements : [] },
}),
JSON . stringify ({
timestamp : '2026-06-14T18:12:03.000Z' ,
type : 'event_msg' ,
payload : { type : 'agent_message' , phase : 'final_answer' , message : '{"outcome":"allow"}' },
}),
'' ,
]. join ( '\n' ));
const dbPath = join ( home , '.obelisk' , 'obelisk.sqlite' );
const result = buildIndex ({ claudeDir , codexDir , dbPath , DatabaseImpl : TestDatabase });
const db = new TestDatabase ( dbPath );
assert . deepEqual ( result . affectedSessionIds , []);
assert . equal ( db . prepare ( 'SELECT COUNT(*) AS c FROM sessions WHERE id=?' ). get ( `codex: ${ guardianId } ` ). c , 0 );
assert . equal ( db . prepare ( 'SELECT COUNT(*) AS c FROM messages WHERE session_id=?' ). get ( `codex: ${ guardianId } ` ). c , 0 );
assert . equal ( db . prepare ( 'SELECT COUNT(*) AS c FROM subagents WHERE agent_id=?' ). get ( `codex: ${ guardianId } ` ). c , 0 );
assert . equal ( db . prepare ( "SELECT COUNT(*) AS c FROM messages_fts WHERE messages_fts MATCH 'approval'" ). get (). c , 0 );
db . close ();
});
test ( 'app indexer removes stale Codex guardian rows when the JSONL was already indexed' , () => {
const home = mkdtempSync ( join ( tmpdir (), 'obelisk-app-indexer-codex-guardian-stale-' ));
const claudeDir = join ( home , '.claude' );
const codexDir = join ( home , '.codex' );
const codexSessionDir = join ( codexDir , 'sessions' , '2026' , '06' , '15' );
mkdirSync ( join ( claudeDir , 'projects' ), { recursive : true });
mkdirSync ( codexSessionDir , { recursive : true });
const guardianId = '019ed5c4-8d52-7bc0-91f3-447a15e987d1' ;
const guardianSessionId = `codex: ${ guardianId } ` ;
const jsonlPath = join ( codexSessionDir , `rollout-2026-06-15T02-12-00- ${ guardianId } .jsonl` );
writeFileSync ( jsonlPath , [
JSON . stringify ({
timestamp : '2026-06-14T18:12:00.000Z' ,
type : 'session_meta' ,
payload : {
id : guardianId ,
timestamp : '2026-06-14T18:12:00.000Z' ,
cwd : '/tmp/obelisk-app' ,
cli_version : '0.135.0-alpha.1' ,
thread_source : 'subagent' ,
source : { subagent : { other : 'guardian' } },
},
}),
JSON . stringify ({
timestamp : '2026-06-14T18:12:01.000Z' ,
type : 'event_msg' ,
payload : { type : 'user_message' , message : 'stale approval guardian prompt' , images : [], local_images : [], text_elements : [] },
}),
'' ,
]. join ( '\n' ));
const dbPath = join ( home , '.obelisk' , 'obelisk.sqlite' );
buildIndex ({ claudeDir , codexDir : join ( home , 'empty-codex' ), dbPath , DatabaseImpl : TestDatabase });
const db = new TestDatabase ( dbPath );
db . prepare ( 'INSERT INTO sessions (id,jsonl_path,source,message_count) VALUES (?,?,?,?)' ). run ( guardianSessionId , jsonlPath , 'codex' , 1 );
db . prepare ( 'INSERT INTO messages (uuid,session_id,type,timestamp,role,text,content_type,source) VALUES (?,?,?,?,?,?,?,?)' )
. run ( ` ${ guardianSessionId } :000002` , guardianSessionId , 'user' , '2026-06-14T18:12:01.000Z' , 'user' , 'stale approval guardian prompt' , 'text' , 'codex' );
db . prepare ( 'INSERT INTO tool_calls (id,message_uuid,session_id,name,input_json,file_path) VALUES (?,?,?,?,?,?)' )
. run ( 'codex:call_guardian' , ` ${ guardianSessionId } :000002` , guardianSessionId , 'exec_command' , '{}' , null );
db . prepare ( 'INSERT INTO tool_results (tool_use_id,message_uuid,session_id,content,file_path,is_error) VALUES (?,?,?,?,?,?)' )
. run ( 'codex:call_guardian' , ` ${ guardianSessionId } :000002` , guardianSessionId , 'ok' , null , 0 );
db . prepare ( 'INSERT INTO subagents (agent_id,session_id) VALUES (?,?)' ). run ( guardianSessionId , guardianSessionId );
db . prepare ( 'INSERT OR REPLACE INTO index_state (jsonl_path,mtime,lines_processed) VALUES (?,?,?)' )
. run ( jsonlPath , statSync ( jsonlPath ). mtimeMs , 2 );
db . close ();
buildIndex ({ claudeDir , codexDir , dbPath , DatabaseImpl : TestDatabase });
const cleanedDb = new TestDatabase ( dbPath );
assert . equal ( cleanedDb . prepare ( 'SELECT COUNT(*) AS c FROM sessions WHERE id=?' ). get ( guardianSessionId ). c , 0 );
assert . equal ( cleanedDb . prepare ( 'SELECT COUNT(*) AS c FROM messages WHERE session_id=?' ). get ( guardianSessionId ). c , 0 );
assert . equal ( cleanedDb . prepare ( 'SELECT COUNT(*) AS c FROM tool_calls WHERE session_id=?' ). get ( guardianSessionId ). c , 0 );
assert . equal ( cleanedDb . prepare ( 'SELECT COUNT(*) AS c FROM tool_results WHERE session_id=?' ). get ( guardianSessionId ). c , 0 );
assert . equal ( cleanedDb . prepare ( 'SELECT COUNT(*) AS c FROM subagents WHERE agent_id=?' ). get ( guardianSessionId ). c , 0 );
assert . equal ( cleanedDb . prepare ( "SELECT COUNT(*) AS c FROM messages_fts WHERE messages_fts MATCH 'stale'" ). get (). c , 0 );
cleanedDb . close ();
});
test ( 'app indexer maps Codex subagent threads onto parent sessions' , () => {
const home = mkdtempSync ( join ( tmpdir (), 'obelisk-app-indexer-codex-subagent-' ));
const claudeDir = join ( home , '.claude' );
const codexDir = join ( home , '.codex' );
const codexSessionDir = join ( codexDir , 'sessions' , '2026' , '06' , '15' );
mkdirSync ( join ( claudeDir , 'projects' ), { recursive : true });
mkdirSync ( codexSessionDir , { recursive : true });
const parentId = '019ec6ee-cebd-7431-9c93-ceec89a98a5f' ;
const childId = '019ec739-9f75-7a02-ba2a-371986e23823' ;
writeFileSync ( join ( codexSessionDir , `rollout-2026-06-15T00-19-59- ${ parentId } .jsonl` ), [
JSON . stringify ({
timestamp : '2026-06-14T16:19:59.842Z' ,
type : 'session_meta' ,
payload : {
id : parentId ,
timestamp : '2026-06-14T16:19:59.842Z' ,
cwd : '/tmp/obelisk-app' ,
cli_version : '0.135.0-alpha.1' ,
source : 'vscode' ,
git : { branch : 'feat/codex' },
},
}),
JSON . stringify ({
timestamp : '2026-06-14T16:20:00.000Z' ,
type : 'event_msg' ,
payload : {
type : 'collab_agent_spawn_end' ,
call_id : 'call_spawn_1' ,
sender_thread_id : parentId ,
new_thread_id : childId ,
new_agent_nickname : 'Plato' ,
new_agent_role : 'worker' ,
model : 'gpt-5.5' ,
reasoning_effort : 'xhigh' ,
prompt : 'inspect app-side codex indexing' ,
status : 'pending_init' ,
},
}),
'' ,
]. join ( '\n' ));
writeFileSync ( join ( codexSessionDir , `rollout-2026-06-15T01-41-42- ${ childId } .jsonl` ), [
JSON . stringify ({
timestamp : '2026-06-14T17:41:42.924Z' ,
type : 'session_meta' ,
payload : {
id : childId ,
timestamp : '2026-06-14T17:41:42.924Z' ,
cwd : '/tmp/obelisk-app' ,
cli_version : '0.135.0-alpha.1' ,
source : {
subagent : {
thread_spawn : {
parent_thread_id : parentId ,
depth : 1 ,
agent_nickname : 'Plato' ,
agent_role : 'worker' ,
},
},
},
agent_nickname : 'Plato' ,
agent_role : 'worker' ,
},
}),
JSON . stringify ({
timestamp : '2026-06-14T17:41:43.000Z' ,
type : 'event_msg' ,
payload : { type : 'user_message' , message : 'subagent prompt' , images : [], local_images : [], text_elements : [] },
}),
JSON . stringify ({
timestamp : '2026-06-14T17:41:44.000Z' ,
type : 'event_msg' ,
payload : { type : 'agent_message' , phase : 'final_answer' , message : 'subagent answer' },
}),
JSON . stringify ({
timestamp : '2026-06-14T17:41:45.000Z' ,
type : 'response_item' ,
payload : { type : 'function_call' , call_id : 'call_child_1' , name : 'exec_command' , arguments : '{"cmd":"pwd"}' },
}),
JSON . stringify ({
timestamp : '2026-06-14T17:41:46.000Z' ,
type : 'response_item' ,
payload : { type : 'function_call_output' , call_id : 'call_child_1' , output : '/tmp/obelisk-app' },
}),
'' ,
]. join ( '\n' ));
const dbPath = join ( home , '.obelisk' , 'obelisk.sqlite' );
buildIndex ({ claudeDir , codexDir , dbPath , DatabaseImpl : TestDatabase });
const db = new TestDatabase ( dbPath );
assert . equal ( db . prepare ( 'SELECT COUNT(*) AS c FROM sessions WHERE id=?' ). get ( `codex: ${ childId } ` ). c , 0 );
const subagent = db . prepare ( 'SELECT * FROM subagents WHERE agent_id=?' ). get ( `codex: ${ childId } ` );
assert . equal ( subagent . session_id , `codex: ${ parentId } ` );
assert . equal ( subagent . parent_tool_use_id , 'codex:call_spawn_1' );
assert . equal ( subagent . agent_type , 'worker' );
assert . equal ( subagent . description , 'Plato' );
const spawnMessage = db . prepare ( 'SELECT * FROM messages WHERE uuid=?' ). get ( `codex: ${ parentId } :000002` );
assert . equal ( spawnMessage . session_id , `codex: ${ parentId } ` );
assert . equal ( spawnMessage . content_type , 'tool_use' );
assert . equal ( spawnMessage . source , 'codex' );
assert . equal ( db . prepare ( 'SELECT name, message_uuid FROM tool_calls WHERE id=?' ). get ( 'codex:call_spawn_1' ). message_uuid , spawnMessage . uuid );
const childMessages = db . prepare ( 'SELECT session_id, agent_id, is_sidechain, source, text FROM messages WHERE agent_id=? ORDER BY timestamp, uuid' ). all ( `codex: ${ childId } ` );
assert . deepEqual ( childMessages . map ( m => [ m . session_id , m . agent_id , m . is_sidechain , m . source , m . text ]), [
[ `codex: ${ parentId } ` , `codex: ${ childId } ` , 1 , 'codex' , 'subagent prompt' ],
[ `codex: ${ parentId } ` , `codex: ${ childId } ` , 1 , 'codex' , 'subagent answer' ],
[ `codex: ${ parentId } ` , `codex: ${ childId } ` , 1 , 'codex' , null ],
]);
const childResult = db . prepare ( `
SELECT tr.content FROM tool_results tr
JOIN messages m ON m.uuid = tr.message_uuid
WHERE m.agent_id = ?
` ). get ( `codex: ${ childId } ` );
assert . equal ( childResult . content , '/tmp/obelisk-app' );
db . close ();
});