import { test } from 'node:test'; import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; import { mkdtempSync, mkdirSync, statSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { spawnSync } from 'node:child_process'; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const require = createRequire(import.meta.url); const { DatabaseSync } = require('node:sqlite'); function runRuntime(args, { home }) { return spawnSync(process.execPath, ['packages/core/src/runtime.mjs', ...args], { cwd: repoRoot, env: { ...process.env, HOME: home }, encoding: 'utf8', }); } function tempHome() { const home = mkdtempSync(join(tmpdir(), 'obelisk-runtime-home-')); mkdirSync(join(home, '.claude'), { recursive: true }); return home; } test('runtime query scripts cannot call attune helpers', () => { const home = tempHome(); const scriptPath = join(home, 'query.mjs'); writeFileSync(scriptPath, 'return { rememberType: typeof remember, forgetType: typeof forget, overviewType: typeof overview };'); const result = runRuntime(['--query', scriptPath], { home }); assert.equal(result.status, 0, result.stderr || result.stdout); assert.deepEqual(JSON.parse(result.stdout), { rememberType: 'undefined', forgetType: 'undefined', overviewType: 'function', }); }); test('runtime attune scripts expose only memory mutation helpers', () => { const home = tempHome(); const memoryPath = join(home, 'memory.md'); const scriptPath = join(home, 'attune.mjs'); writeFileSync(memoryPath, '# Memory\n'); writeFileSync(scriptPath, ` return { rememberType: typeof remember, forgetType: typeof forget, searchType: typeof search, sqlType: typeof sql, overviewType: typeof overview, result: remember({ path: ${JSON.stringify(memoryPath)}, project: 'runtime-test', summary: 'Decision: runtime remember exposes only memory registration.' }) }; `); const result = runRuntime(['--attune', scriptPath], { home }); assert.equal(result.status, 0, result.stderr || result.stdout); const payload = JSON.parse(result.stdout); assert.equal(payload.rememberType, 'function'); assert.equal(payload.forgetType, 'function'); assert.equal(payload.searchType, 'undefined'); assert.equal(payload.sqlType, 'undefined'); assert.equal(payload.overviewType, 'undefined'); assert.equal(payload.result.path, memoryPath); }); test('runtime rejects removed remember mode', () => { const home = tempHome(); const scriptPath = join(home, 'remember.mjs'); writeFileSync(scriptPath, 'return { ok: true };'); const result = runRuntime(['--remember', scriptPath], { home }); assert.notEqual(result.status, 0); assert.equal(result.stdout, ''); assert.match(result.stderr, /--attune /); }); test('runtime migrates old memories schema before recall', () => { const home = tempHome(); const dbPath = join(home, '.claude', 'obelisk.sqlite'); const db = new DatabaseSync(dbPath); db.exec(` CREATE TABLE memories ( id TEXT PRIMARY KEY, session_id TEXT, project TEXT, message_start TEXT, message_end TEXT, path TEXT, summary TEXT, created_at TEXT ); INSERT INTO memories (id, project, path, summary, created_at) VALUES ('mem-old', 'legacy-project', '/tmp/old.md', 'Decision: keep legacy memory rows readable.', '2026-06-10T12:00:00Z'); `); db.close(); const scriptPath = join(home, 'query.mjs'); writeFileSync(scriptPath, ` return memories({ project: "%legacy-project%", query: "legacy memory", limit: 5 }) .map(m => ({ id: m.id, rankType: typeof m.rank })); `); const result = runRuntime(['--query', scriptPath], { home }); assert.equal(result.status, 0, result.stderr || result.stdout); assert.deepEqual(JSON.parse(result.stdout), [{ id: 'mem-old', rankType: 'number' }]); }); test('runtime indexes Codex root sessions into the shared query helpers', () => { const home = tempHome(); const codexSessionDir = join(home, '.codex', 'sessions', '2026', '06', '15'); 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-runtime', 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: 'user_message', message: 'codex user asks for runtime indexing', images: [], local_images: [], text_elements: [] }, }), JSON.stringify({ timestamp: '2026-06-14T16:20:01.000Z', type: 'response_item', payload: { type: 'message', role: 'developer', content: [{ type: 'input_text', text: 'developer replay should stay out of visible search' }], }, }), JSON.stringify({ timestamp: '2026-06-14T16:20:02.000Z', type: 'event_msg', payload: { type: 'agent_message', phase: 'final_answer', message: 'codex assistant replies from runtime' }, }), JSON.stringify({ timestamp: '2026-06-14T16:20:03.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:04.000Z', type: 'response_item', payload: { type: 'function_call_output', call_id: 'call_codex_1', output: '/tmp/obelisk-runtime' }, }), '', ].join('\n')); const scriptPath = join(home, 'query.mjs'); writeFileSync(scriptPath, ` const sid = ${JSON.stringify(`codex:${codexId}`)}; return { sessions: sessions({ source: 'codex', limit: 5 }).map(s => ({ id: s.id, source: s.source, project: s.project, project_path: s.project_path, git_branch: s.git_branch, version: s.version, message_count: s.message_count })), messages: thread(sid).map(m => ({ role: m.role, text: m.text, source: m.source, content_type: m.content_type })), search: search('runtime indexing', { source: 'codex', limit: 5 }).map(h => ({ uuid: h.message.uuid, message_source: h.message.source, session_source: h.session.source })), developerReplay: search('developer replay', { source: 'codex', limit: 5 }).length, rawHasEventLine: raw(${JSON.stringify(`codex:${codexId}:000002`)}, { limit: 1000 })?.text.includes('codex user asks for runtime indexing') || false, tool: sql('SELECT id, message_uuid, session_id, name FROM tool_calls WHERE id=?', 'codex:call_codex_1')[0], toolResult: sql('SELECT tool_use_id, message_uuid, session_id, content FROM tool_results WHERE tool_use_id=?', 'codex:call_codex_1')[0], overviewSources: overview({ limit: 5 }).totals.sources }; `); const result = runRuntime(['--query', scriptPath], { home }); assert.equal(result.status, 0, result.stderr || result.stdout); const payload = JSON.parse(result.stdout); assert.deepEqual(payload.sessions, [{ id: `codex:${codexId}`, source: 'codex', project: '-tmp-obelisk-runtime', project_path: '/tmp/obelisk-runtime', git_branch: 'feat/codex', version: '0.135.0-alpha.1', message_count: 3, }]); assert.deepEqual(payload.messages.map(m => [m.role, m.text, m.source, m.content_type]), [ ['user', 'codex user asks for runtime indexing', 'codex', 'text'], ['assistant', 'codex assistant replies from runtime', 'codex', 'text'], ['assistant', null, 'codex', 'tool_use'], ]); assert.equal(payload.search[0].message_source, 'codex'); assert.equal(payload.search[0].session_source, 'codex'); assert.equal(payload.developerReplay, 0); assert.equal(payload.rawHasEventLine, true); assert.equal(payload.tool.session_id, `codex:${codexId}`); assert.equal(payload.tool.message_uuid, `codex:${codexId}:000005`); assert.equal(payload.toolResult.message_uuid, `codex:${codexId}:000005`); assert.equal(payload.toolResult.content, '/tmp/obelisk-runtime'); assert.ok(payload.overviewSources.some(s => s.source === 'codex' && s.session_count === 1)); }); test('runtime skips Codex guardian review threads', () => { const home = tempHome(); const codexSessionDir = join(home, '.codex', 'sessions', '2026', '06', '15'); 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-runtime', 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-runtime', 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 scriptPath = join(home, 'query.mjs'); writeFileSync(scriptPath, ` const sid = ${JSON.stringify(`codex:${guardianId}`)}; return { sessions: sessions({ source: 'codex', limit: 5 }).map(s => s.id), searchCount: search('approval', { source: 'codex', limit: 5 }).length, sessionRows: sql('SELECT COUNT(*) AS c FROM sessions WHERE id=?', sid)[0].c, messageRows: sql('SELECT COUNT(*) AS c FROM messages WHERE session_id=?', sid)[0].c, subagentRows: sql('SELECT COUNT(*) AS c FROM subagents WHERE agent_id=?', sid)[0].c }; `); const result = runRuntime(['--query', scriptPath], { home }); assert.equal(result.status, 0, result.stderr || result.stdout); assert.deepEqual(JSON.parse(result.stdout), { sessions: [], searchCount: 0, sessionRows: 0, messageRows: 0, subagentRows: 0, }); }); test('runtime removes stale Codex guardian rows when the JSONL was already indexed', () => { const home = tempHome(); const codexSessionDir = join(home, '.codex', 'sessions', '2026', '06', '15'); mkdirSync(codexSessionDir, { recursive: true }); const initScriptPath = join(home, 'init.mjs'); writeFileSync(initScriptPath, 'return sessions({ source: "codex", limit: 5 }).length;'); assert.equal(runRuntime(['--query', initScriptPath], { home }).status, 0); 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-runtime', 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 db = new DatabaseSync(join(home, '.obelisk', 'obelisk.sqlite')); 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.prepare("UPDATE index_state SET mtime=? WHERE jsonl_path='__last_build__'").run(Date.now() - 31000); db.close(); const scriptPath = join(home, 'query.mjs'); writeFileSync(scriptPath, ` const sid = ${JSON.stringify(guardianSessionId)}; return { sessions: sessions({ source: 'codex', limit: 5 }).map(s => s.id), searchCount: search('stale', { source: 'codex', limit: 5 }).length, sessionRows: sql('SELECT COUNT(*) AS c FROM sessions WHERE id=?', sid)[0].c, messageRows: sql('SELECT COUNT(*) AS c FROM messages WHERE session_id=?', sid)[0].c, toolRows: sql('SELECT COUNT(*) AS c FROM tool_calls WHERE session_id=?', sid)[0].c, resultRows: sql('SELECT COUNT(*) AS c FROM tool_results WHERE session_id=?', sid)[0].c, subagentRows: sql('SELECT COUNT(*) AS c FROM subagents WHERE agent_id=?', sid)[0].c }; `); const result = runRuntime(['--query', scriptPath], { home }); assert.equal(result.status, 0, result.stderr || result.stdout); assert.deepEqual(JSON.parse(result.stdout), { sessions: [], searchCount: 0, sessionRows: 0, messageRows: 0, toolRows: 0, resultRows: 0, subagentRows: 0, }); }); test('runtime maps Codex child threads onto subagents', () => { const home = tempHome(); const codexSessionDir = join(home, '.codex', 'sessions', '2026', '06', '15'); 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-runtime', cli_version: '0.135.0-alpha.1', source: 'vscode', }, }), 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', prompt: 'inspect skill-side codex indexing', }, }), '', ].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-runtime', cli_version: '0.135.0-alpha.1', source: { subagent: { thread_spawn: { parent_thread_id: parentId, 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' }, }), '', ].join('\n')); const scriptPath = join(home, 'query.mjs'); writeFileSync(scriptPath, ` return { parentSessions: sessions({ source: 'codex', limit: 5 }).map(s => s.id), subagents: subagents({ source: 'codex', limit: 5 }).map(sa => ({ agent_id: sa.agent_id, session_id: sa.session_id, parent_tool_use_id: sa.parent_tool_use_id, agent_type: sa.agent_type, description: sa.description, messageCount: sa.messageCount })), childMessages: sql( 'SELECT session_id, agent_id, is_sidechain, source, text FROM messages WHERE agent_id=? ORDER BY timestamp, uuid', ${JSON.stringify(`codex:${childId}`)} ) }; `); const result = runRuntime(['--query', scriptPath], { home }); assert.equal(result.status, 0, result.stderr || result.stdout); const payload = JSON.parse(result.stdout); assert.deepEqual(payload.parentSessions, [`codex:${parentId}`]); assert.deepEqual(payload.subagents, [{ agent_id: `codex:${childId}`, session_id: `codex:${parentId}`, parent_tool_use_id: 'codex:call_spawn_1', agent_type: 'worker', description: 'Plato', messageCount: 2, }]); assert.deepEqual(payload.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'], ]); });