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); const { buildIndex } = require('../app/src/main/indexer.js'); 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: 'Keep answers concise' }] }, }), JSON.stringify({ uuid: 'msg-meta-local-command', type: 'user', timestamp: '2026-06-13T10:01:00Z', message: { role: 'user', content: [{ type: 'text', text: 'git status' }] }, }), 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(); });