refactor: add canonical transcript assembly seam

This commit is contained in:
tommy0103
2026-07-21 00:58:34 +08:00
parent 3ee44de4e5
commit f79f1b3e3b
39 changed files with 1741 additions and 670 deletions
+9 -7
View File
@@ -7,7 +7,7 @@ import { join } from 'node:path';
const require = createRequire(import.meta.url);
import { buildIndex } from '../app/src/main/indexer.ts';
import { CLAUDE_INPUT_TOKEN_SEMANTICS_MARKER } from '../packages/core/src/providers/claude.ts';
import { CLAUDE_CANONICAL_TRANSCRIPT_MARKER } from '../packages/core/src/providers/claude.ts';
const { DatabaseSync } = require('node:sqlite');
class TestDatabase {
@@ -123,7 +123,7 @@ test('app indexer refreshes unchanged Claude usage when input token semantics ch
const stale = new TestDatabase(dbPath);
stale.prepare('UPDATE messages SET input_tokens = 10 WHERE uuid = ?').run('msg-token-semantics-1');
stale.prepare('DELETE FROM index_state WHERE jsonl_path = ?')
.run(CLAUDE_INPUT_TOKEN_SEMANTICS_MARKER);
.run(CLAUDE_CANONICAL_TRANSCRIPT_MARKER);
stale.close();
buildIndex({
@@ -140,7 +140,7 @@ test('app indexer refreshes unchanged Claude usage when input token semantics ch
);
assert.ok(
refreshed.prepare('SELECT jsonl_path FROM index_state WHERE jsonl_path = ?')
.get(CLAUDE_INPUT_TOKEN_SEMANTICS_MARKER),
.get(CLAUDE_CANONICAL_TRANSCRIPT_MARKER),
);
refreshed.close();
});
@@ -437,11 +437,12 @@ test('app indexer loads Codex root sessions into the shared schema', () => {
]);
assert.equal(messages[1].turn_duration_ms, 4321);
const tool = db.prepare('SELECT * FROM tool_calls WHERE id=?').get('codex:call_codex_1');
const toolId = `codex:${codexId}:call_codex_1`;
const tool = db.prepare('SELECT * FROM tool_calls WHERE id=?').get(toolId);
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');
const toolResult = db.prepare('SELECT message_uuid, content FROM tool_results WHERE tool_use_id=?').get(toolId);
assert.equal(toolResult.message_uuid, `codex:${codexId}:000004`);
assert.equal(toolResult.content, '/tmp/obelisk-app');
db.close();
@@ -771,7 +772,8 @@ test('app indexer maps Codex subagent threads onto parent sessions', () => {
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');
const spawnToolId = `codex:${parentId}:call_spawn_1`;
assert.equal(subagent.parent_tool_use_id, spawnToolId);
assert.equal(subagent.agent_type, 'worker');
assert.equal(subagent.description, 'Plato');
@@ -779,7 +781,7 @@ test('app indexer maps Codex subagent threads onto parent sessions', () => {
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);
assert.equal(db.prepare('SELECT name, message_uuid FROM tool_calls WHERE id=?').get(spawnToolId).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]), [
+45 -2
View File
@@ -6,6 +6,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { buildIndex } from '../app/src/main/indexer.ts';
import { createKimiProvider } from '../packages/core/src/providers/kimi.ts';
const require = createRequire(import.meta.url);
const { DatabaseSync } = require('node:sqlite');
@@ -21,7 +22,7 @@ class TestDatabase {
close() { return this.db.close(); }
}
function writeSession(kimiDir) {
function writeSession(kimiDir, { userSlash = false } = {}) {
const sessionDir = join(kimiDir, 'sessions', 'workspace-1', 'session-index-1');
const mainDir = join(sessionDir, 'agents', 'main');
mkdirSync(mainDir, { recursive: true });
@@ -35,7 +36,15 @@ function writeSession(kimiDir) {
const wirePath = join(mainDir, 'wire.jsonl');
const records = [
{ type: 'metadata', protocol_version: '1.5', created_at: 1753005600000 },
{ type: 'context.append_message', time: 1753005601000, message: { role: 'user', content: [{ type: 'text', text: 'kimi index needle' }], toolCalls: [], origin: { kind: 'user' } } },
{ type: 'context.append_message', time: 1753005601000, message: userSlash
? {
role: 'user', content: 'Expanded skill instructions.', toolCalls: [],
origin: {
kind: 'skill_activation', trigger: 'user-slash', skillName: 'obelisk',
skillArgs: 'find prior decisions',
},
}
: { role: 'user', content: [{ type: 'text', text: 'kimi index needle' }], toolCalls: [], origin: { kind: 'user' } } },
];
writeFileSync(wirePath, records.map((record) => JSON.stringify(record)).join('\n') + '\n');
return { sessionDir, wirePath, records };
@@ -123,3 +132,37 @@ test('Kimi undo and clear replace the indexed session instead of leaving stale r
assert.equal(db.prepare('SELECT message_count FROM sessions WHERE id=?').get('kimi:session-index-1').message_count, 0);
db.close();
});
test('Kimi prompt semantics marker replays unchanged sessions once', () => {
const home = mkdtempSync(join(tmpdir(), 'obelisk-kimi-prompt-marker-'));
const claudeDir = join(home, '.claude');
const codexDir = join(home, '.codex');
const kimiDir = join(home, '.kimi-code');
const dbPath = join(home, '.obelisk', 'obelisk.sqlite');
writeSession(kimiDir, { userSlash: true });
const options = {
claudeDir,
codexDir,
providerRoots: { kimi: kimiDir },
dbPath,
DatabaseImpl: TestDatabase,
};
buildIndex(options);
let db = new TestDatabase(dbPath);
const marker = createKimiProvider({ rootDir: kimiDir }).indexVersionMarker;
assert.equal(typeof marker, 'string');
db.prepare("UPDATE messages SET text='stale expanded instructions', is_meta=1 WHERE source='kimi'").run();
db.prepare('DELETE FROM index_state WHERE jsonl_path=?').run(marker);
db.close();
const replay = buildIndex(options);
assert.deepEqual(replay.affectedSessionIds, ['kimi:session-index-1']);
db = new TestDatabase(dbPath);
assert.deepEqual(
{ ...db.prepare("SELECT text,is_meta FROM messages WHERE source='kimi'").get() },
{ text: '/obelisk find prior decisions', is_meta: 0 },
);
assert.equal(db.prepare('SELECT COUNT(*) AS c FROM index_state WHERE jsonl_path=?').get(marker).c, 1);
db.close();
});
+1
View File
@@ -258,6 +258,7 @@ test('main process watches every root declared by the built-in provider registry
assert.equal(serviceOptions.length, 1);
assert.deepEqual(serviceOptions[0].watchDirs, [
join(claudeDir, 'projects'),
join(claudeDir, 'history.jsonl'),
join(codexDir, 'sessions'),
join(codexDir, 'session_index.jsonl'),
join(home, '.kimi-code', 'sessions'),
+1 -1
View File
@@ -41,7 +41,7 @@ test('app indexer persists every provider through one registry-driven loop', ()
yield {
kind: 'message', uuid: 'alpha:message', session_id: unit.sessionId, type: 'user',
parent_uuid: null, timestamp: '2026-07-20T10:00:00.000Z', role: 'user',
text: 'registry tracer bullet', content_type: 'text', is_meta: 0, model: null,
text: 'registry tracer bullet', content_type: 'text', is_meta: 0, visibility: 'visible', model: null,
is_sidechain: 0, agent_id: null, input_tokens: null, output_tokens: null,
cwd: '/tmp/alpha', skill: null, source: 'alpha',
};
+1 -7
View File
@@ -223,13 +223,7 @@ test('BEGIN contention during finalize defers the build', () => {
test('a finalize database error is propagated instead of swallowed as malformed input', () => {
const { home, dbPath, projectsDir } = twoFileHome('hello alpha', 'hello beta');
const workflowDir = join(projectsDir, '-tmp-proj', 'alpha', 'workflows');
mkdirSync(workflowDir, { recursive: true });
writeFileSync(join(workflowDir, 'run.json'), JSON.stringify({
runId: 'workflow-1',
workflowName: 'POISON WORKFLOW',
}));
const Db = makeDbClass(args => args.some(arg => typeof arg === 'string' && arg.includes('POISON WORKFLOW')));
const Db = makeDbClass(args => args.some(arg => arg === '__last_build__'));
assert.throws(() => buildIndex({
force: false,
+74 -2
View File
@@ -4,11 +4,16 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, writeFileSync, statSync } from 'node:fs';
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';
import { parse } from '../packages/core/src/providers/claude.ts';
import { createClaudeProvider, parse } from '../packages/core/src/providers/claude.ts';
import { assembleSessionDetail } from '../packages/core/src/session-detail.ts';
import { persist } from '../packages/core/src/persist.ts';
const SCHEMA = readFileSync(new URL('../packages/core/src/schema.sql', import.meta.url), 'utf8');
function writeFixture() {
const dir = mkdtempSync(join(tmpdir(), 'obelisk-claude-parse-'));
@@ -69,6 +74,10 @@ test('claude parse() yields the expected record stream for a main session', () =
assert.equal(sessions[0].ended_at, '2026-06-10T10:00:10Z');
assert.equal(sessions[0].git_branch, 'main');
const detail = assembleSessionDetail(values);
assert.deepEqual(detail.messages.map((message) => message.text), ['hi', 'ok']);
assert.equal(detail.messages[1].tool_calls[0].result.content, 'file body');
// Cursor encodes mtime:lines (6 lines consumed).
assert.equal(ret, `${statSync(path).mtimeMs}:6`);
});
@@ -90,3 +99,66 @@ test('claude parse() resumes from a cursor, skipping already-indexed lines', ()
assert.deepEqual(values.filter(r => r.kind !== 'session'), []);
assert.equal(values.find(r => r.kind === 'session').message_count, 0);
});
test('claude provider emits workflow artifacts with an explicit canonical tool edge', () => {
const root = mkdtempSync(join(tmpdir(), 'obelisk-claude-workflow-'));
const projectDir = join(root, 'projects', '-proj');
const workflowDir = join(projectDir, 'sid-workflow', 'workflows');
const workflowAgentDir = join(projectDir, 'sid-workflow', 'subagents', 'workflows', 'run-workflow');
mkdirSync(workflowDir, { recursive: true });
mkdirSync(workflowAgentDir, { recursive: true });
writeFileSync(join(projectDir, 'sid-workflow.jsonl'), [
{
uuid: 'assistant-workflow', type: 'assistant', timestamp: '2026-06-10T10:00:00Z',
message: { role: 'assistant', content: [{ type: 'tool_use', id: 'workflow-tool', name: 'Workflow', input: {} }] },
},
{
uuid: 'workflow-result', type: 'user', timestamp: '2026-06-10T10:00:01Z',
message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'workflow-tool', content: 'run-workflow complete' }] },
},
].map(line => JSON.stringify(line)).join('\n') + '\n');
writeFileSync(join(workflowDir, 'run-workflow.json'), JSON.stringify({
runId: 'run-workflow',
workflowName: 'Review',
status: 'complete',
workflowProgress: [{ type: 'workflow_agent', agentId: '7', phaseTitle: 'review', label: 'Reviewer' }],
}));
writeFileSync(join(workflowAgentDir, 'agent-7.jsonl'), `${JSON.stringify({
uuid: 'workflow-agent-message', type: 'user', timestamp: '2026-06-10T10:00:00Z',
message: { role: 'user', content: 'review it' },
})}\n`);
writeFileSync(join(workflowAgentDir, 'agent-7.meta.json'), JSON.stringify({
agentType: 'reviewer', description: 'Review the implementation',
}));
writeFileSync(join(root, 'history.jsonl'), `${JSON.stringify({
sessionId: 'sid-workflow', title: 'History-owned title',
})}\n`);
const provider = createClaudeProvider({ rootDir: root });
const units = provider.discover({ lastCursor: () => null });
const records = units.flatMap(unit => drain(provider.parse(unit, null)).values);
const workflow = records.find(record => record.kind === 'workflow');
assert.equal(workflow.parent_tool_use_id, 'workflow-tool');
const detail = assembleSessionDetail(records);
assert.equal(detail.session.title, 'History-owned title');
assert.equal(detail.messages[0].tool_calls[0].workflow.run_id, 'run-workflow');
assert.equal(detail.workflows[0].agents[0].label, 'Reviewer');
assert.equal(detail.workflows[0].agents.length, 1);
const db = new DatabaseSync(':memory:');
db.exec(SCHEMA);
for (const unit of units) persist(db, unit, provider.parse(unit, null));
const workflows = db.prepare('SELECT * FROM workflows').all();
for (const row of workflows) row.agents = db.prepare('SELECT * FROM workflow_agents WHERE run_id=?').all(row.run_id);
const persistedDetail = assembleSessionDetail({
session: db.prepare('SELECT * FROM sessions').get(),
messages: db.prepare('SELECT * FROM messages ORDER BY timestamp, uuid').all(),
toolCalls: db.prepare('SELECT * FROM tool_calls').all(),
toolResults: db.prepare('SELECT * FROM tool_results').all(),
subagents: db.prepare('SELECT * FROM subagents').all(),
workflows,
summaries: db.prepare('SELECT * FROM summaries').all(),
});
assert.deepEqual(persistedDetail, detail);
db.close();
});
+26 -4
View File
@@ -5,11 +5,11 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, writeFileSync } from 'node:fs';
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { parse } from '../packages/core/src/providers/codex.ts';
import { createCodexProvider, parse } from '../packages/core/src/providers/codex.ts';
function writeFixture(lines) {
const dir = mkdtempSync(join(tmpdir(), 'obelisk-codex-parse-'));
@@ -56,9 +56,9 @@ test('codex parse() yields a deduped, tool-aware record stream with a total sess
assert.equal(textAssistant.output_tokens, 50);
// Tool call + result.
assert.deepEqual(byKind('tool_call').map(t => ({ id: t.id, name: t.name })), [{ id: 'codex:call_1', name: 'shell' }]);
assert.deepEqual(byKind('tool_call').map(t => ({ id: t.id, name: t.name })), [{ id: `codex:${META.id}:call_1`, name: 'shell' }]);
assert.equal(byKind('tool_result').length, 1);
assert.equal(byKind('tool_result')[0].tool_use_id, 'codex:call_1');
assert.equal(byKind('tool_result')[0].tool_use_id, `codex:${META.id}:call_1`);
// task_complete → turn duration on the text-assistant message.
assert.deepEqual(byKind('message-turn-duration').map(d => d.turn_duration_ms), [1500]);
@@ -84,3 +84,25 @@ test('codex parse() retracts a guardian thread via delete-session and emits noth
assert.equal(values[0].kind, 'delete-session');
assert.match(values[0].sessionId, /^codex:/);
});
test('codex provider folds session_index metadata into its canonical session record', () => {
const root = mkdtempSync(join(tmpdir(), 'obelisk-codex-index-meta-'));
const sessionsDir = join(root, 'sessions', '2026', '06', '10');
mkdirSync(sessionsDir, { recursive: true });
const path = join(sessionsDir, `rollout-${META.id}.jsonl`);
writeFileSync(path, `${JSON.stringify({
type: 'session_meta', timestamp: '2026-06-10T10:00:00Z', payload: META,
})}\n`);
const indexPath = join(root, 'session_index.jsonl');
writeFileSync(indexPath, `${JSON.stringify({
id: META.id, thread_name: 'Indexed title', updated_at: '2026-06-10T11:00:00Z',
})}\n`);
const provider = createCodexProvider({ rootDir: root });
const units = provider.discover({ lastCursor: () => '9999999999999:1', changedPaths: [indexPath] });
assert.equal(units.length, 1);
const { values } = drain(provider.parse(units[0], null));
const session = values.find(record => record.kind === 'session');
assert.equal(session.title, 'Indexed title');
assert.equal(session.ended_at, '2026-06-10T11:00:00Z');
});
+69
View File
@@ -0,0 +1,69 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { DatabaseSync } from 'node:sqlite';
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { assembleSessionDetail } from '../app/src/shared/session-detail-assembly.mjs';
import { persist } from '../packages/core/src/persist.ts';
import { parse } from '../packages/core/src/providers/codex.ts';
const SCHEMA = readFileSync(new URL('../packages/core/src/schema.sql', import.meta.url), 'utf8');
const PARENT_ID = '019ed000-0000-7000-8000-000000000101';
const REPLAY_ID = '019ed000-0000-7000-8000-000000000102';
function writeRollout(path, meta, source) {
writeFileSync(path, [
{ timestamp: '2026-06-15T10:00:00Z', type: 'session_meta', payload: meta },
{
timestamp: '2026-06-15T10:00:01Z',
type: 'response_item',
payload: { type: 'custom_tool_call', call_id: 'call_shared', name: 'exec', input: source },
},
{
timestamp: '2026-06-15T10:00:02Z',
type: 'response_item',
payload: { type: 'custom_tool_call_output', call_id: 'call_shared', output: 'done' },
},
].map(line => JSON.stringify(line)).join('\n') + '\n');
}
test('a replayed Codex call cannot steal the visible message tool association', () => {
const dir = mkdtempSync(join(tmpdir(), 'obelisk-codex-replay-'));
const parentPath = join(dir, 'parent.jsonl');
const replayPath = join(dir, 'replay.jsonl');
writeRollout(parentPath, {
id: PARENT_ID,
timestamp: '2026-06-15T10:00:00Z',
cwd: '/proj',
}, 'text("parent")');
writeRollout(replayPath, {
id: REPLAY_ID,
forked_from_id: PARENT_ID,
timestamp: '2026-06-15T10:00:00Z',
cwd: '/proj',
}, 'text("replay")');
const db = new DatabaseSync(':memory:');
db.exec(SCHEMA);
for (const path of [parentPath, replayPath]) {
const unit = { key: path, sessionId: '', meta: { source: 'codex' } };
persist(db, unit, parse(unit, null));
}
const sessionId = `codex:${PARENT_ID}`;
const messages = db.prepare(
'SELECT * FROM messages WHERE session_id=? AND agent_id IS NULL ORDER BY timestamp, uuid',
).all(sessionId);
const toolCalls = db.prepare('SELECT * FROM tool_calls WHERE session_id=?').all(sessionId);
const toolResults = db.prepare('SELECT * FROM tool_results WHERE session_id=?').all(sessionId);
const assembled = assembleSessionDetail({ messages, toolCalls, toolResults, subagents: [], workflows: [] }).messages;
assert.equal(messages.length, 1, 'the replay remains outside the visible session timeline');
assert.equal(toolCalls.length, 2, 'each rollout owns an independently addressable tool call');
assert.equal(toolResults.length, 2, 'each rollout owns an independently addressable tool result');
assert.deepEqual(assembled[0].tool_calls?.map(call => call.input_json), ['"text(\\"parent\\")"']);
assert.equal(assembled[0].tool_calls?.[0].result?.content, 'done');
db.close();
});
+57 -2
View File
@@ -6,6 +6,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { createKimiProvider } from '../packages/core/src/providers/kimi.ts';
import { assembleSessionDetail } from '../packages/core/src/session-detail.ts';
function drain(gen) {
const values = [];
@@ -71,7 +72,7 @@ test('kimi provider discovers a changed session directory and returns a stable c
assert.deepEqual(unchanged, []);
});
test('kimi provider folds main and subagent wire logs into the existing record language', () => {
test('kimi provider folds main and subagent wire logs into the canonical transcript language', () => {
const { root } = writeKimiFixture();
const provider = createKimiProvider({ rootDir: root });
const unit = provider.discover({ lastCursor: () => null })[0];
@@ -83,7 +84,7 @@ test('kimi provider folds main and subagent wire logs into the existing record l
: record);
assert.equal(
createHash('sha256').update(JSON.stringify(goldenRecords)).digest('hex'),
'ce3c70798bbc50e438605d86eafb28482630ee38dc2baa41a695975c84646822',
'09d76616919435a46a3395194349e696e0d8f6717a24b018515e1f3867ec347a',
'complete yielded record sequence changed',
);
@@ -125,6 +126,10 @@ test('kimi provider folds main and subagent wire logs into the existing record l
assert.deepEqual(byKind('subagent').map((record) => [record.agent_id, record.parent_tool_use_id, record.agent_type]), [
['kimi:session-native-1:agent-7', 'kimi:session-native-1:main:call-1', 'explore'],
]);
const detail = assembleSessionDetail(values);
assert.equal(detail.messages.some((message) => message.text === 'child prompt'), false);
assert.equal(detail.messages.flatMap((message) => message.tool_calls ?? [])[0].result.content.includes('file body'), true);
});
test('kimi provider ignores a torn final wire line until it is completed', () => {
@@ -193,6 +198,56 @@ test('kimi provider scopes changed-path discovery to one session and bypasses an
assert.deepEqual(units.map(unit => unit.key), [firstDir]);
});
test('kimi provider presents user-slash activations as real user prompts', () => {
const root = mkdtempSync(join(tmpdir(), 'obelisk-kimi-user-slash-'));
const sessionDir = join(root, 'sessions', 'workspace-1', 'session-user-slash-1');
const mainDir = join(sessionDir, 'agents', 'main');
mkdirSync(mainDir, { recursive: true });
writeFileSync(join(sessionDir, 'state.json'), JSON.stringify({ workDir: '/tmp/user-slash' }));
const records = [
{ type: 'metadata', protocol_version: '1.5', created_at: 1 },
{ type: 'context.append_message', time: 2, message: {
role: 'user', content: 'User activated the skill and loaded its full instructions.', toolCalls: [],
origin: {
kind: 'skill_activation', trigger: 'user-slash', skillName: 'obelisk',
skillArgs: ' synthesize my history ',
},
} },
{ type: 'context.append_message', time: 3, message: {
role: 'user', content: 'Expanded plugin command implementation.', toolCalls: [],
origin: {
kind: 'plugin_command', trigger: 'user-slash', pluginId: 'demo',
commandName: 'ship', commandArgs: ' --fast ',
},
} },
{ type: 'context.append_message', time: 4, message: {
role: 'user', content: 'Model-triggered skill instructions.', toolCalls: [],
origin: { kind: 'skill_activation', trigger: 'model-tool', skillName: 'review' },
} },
];
writeFileSync(join(mainDir, 'wire.jsonl'), records.map(record => JSON.stringify(record)).join('\n') + '\n');
const provider = createKimiProvider({ rootDir: root });
const unit = provider.discover({ lastCursor: () => null })[0];
const { values } = drain(provider.parse(unit, null));
const messages = values.filter(record => record.kind === 'message');
assert.deepEqual(messages.map(record => ({
text: record.text,
is_meta: record.is_meta,
})), [
{ text: '/obelisk synthesize my history', is_meta: 0 },
{ text: '/demo:ship --fast', is_meta: 0 },
{ text: 'Model-triggered skill instructions.', is_meta: 1 },
]);
assert.equal(provider.raw({
source: 'kimi',
messageUuid: messages[0].uuid,
session: { jsonl_path: join(mainDir, 'wire.jsonl') },
agentId: null,
}).messageText, '/obelisk synthesize my history');
});
test('kimi provider maps protocol-1.0 embedded tool calls and results', () => {
const root = mkdtempSync(join(tmpdir(), 'obelisk-kimi-legacy-tools-'));
const sessionDir = join(root, 'sessions', 'workspace-1', 'session-tools-1');
+26
View File
@@ -92,10 +92,34 @@ test('fresh full re-scan (no prior cursor) resets message_count instead of accum
assert.equal(db.prepare('SELECT COUNT(*) c FROM messages').get().c, 3);
});
test('persist round-trips canonical workflow records and their tool relationship', () => {
const db = freshDb();
function* records() {
yield {
kind: 'workflow', run_id: 'run-1', session_id: 'sid-p', parent_tool_use_id: 'tool-1',
task_id: 'task-1', script: 'review', result_json: '{}', timestamp: '2026-06-10T10:00:00Z',
agent_count: 1, duration_ms: 10, total_tokens: 20, status: 'complete', workflow_name: 'Review',
};
yield {
kind: 'workflow_agent', agent_id: 'agent-1', run_id: 'run-1', session_id: 'sid-p',
agent_type: 'reviewer', phase: 'review', label: 'Reviewer', state: 'complete', tokens: 20,
};
return null;
}
persist(db, { key: 'workflow', sessionId: 'sid-p' }, records());
assert.equal(db.prepare('SELECT parent_tool_use_id FROM workflows WHERE run_id=?').get('run-1').parent_tool_use_id, 'tool-1');
assert.equal(db.prepare('SELECT phase FROM workflow_agents WHERE agent_id=?').get('agent-1').phase, 'review');
db.close();
});
test('delete-session cascades across tables', () => {
const db = freshDb();
const unit = fixtureUnit();
persist(db, unit, parse(unit, null));
db.prepare('INSERT INTO workflows (run_id,session_id) VALUES (?,?)').run('run-delete', 'sid-p');
db.prepare('INSERT INTO workflow_agents (agent_id,run_id,session_id) VALUES (?,?,?)').run('agent-delete', 'run-delete', 'sid-p');
// Hand-roll a one-shot generator emitting a delete for the session.
function* del() { yield { kind: 'delete-session', sessionId: 'sid-p' }; return null; }
@@ -104,4 +128,6 @@ test('delete-session cascades across tables', () => {
assert.equal(db.prepare('SELECT COUNT(*) c FROM sessions WHERE id=?').get('sid-p').c, 0);
assert.equal(db.prepare('SELECT COUNT(*) c FROM messages WHERE session_id=?').get('sid-p').c, 0);
assert.equal(db.prepare('SELECT COUNT(*) c FROM tool_calls WHERE session_id=?').get('sid-p').c, 0);
assert.equal(db.prepare('SELECT COUNT(*) c FROM workflows WHERE session_id=?').get('sid-p').c, 0);
assert.equal(db.prepare('SELECT COUNT(*) c FROM workflow_agents WHERE session_id=?').get('sid-p').c, 0);
});
+1
View File
@@ -70,6 +70,7 @@ test('built-in provider registry exposes every source without caller-side branch
]);
assert.deepEqual(registry.watchRoots(), [
'/sources/claude/projects',
'/sources/claude/history.jsonl',
'/sources/codex/sessions',
'/sources/codex/session_index.jsonl',
'/sources/kimi/sessions',
+2 -2
View File
@@ -3,10 +3,10 @@ import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
test('provider adapters do not change the frozen SQLite schema', () => {
test('canonical transcript persistence schema changes only by explicit decision', () => {
const schema = readFileSync(new URL('../packages/core/src/schema.sql', import.meta.url));
assert.equal(
createHash('sha256').update(schema).digest('hex'),
'3e0615ed2db0d7338561df4d51c4240395714c191aa69567ffcdb70efec49826',
'ef5d0eea6f91c50e78ca5e28ecdc7b3ed5db83db59200642cc25866158f9d307',
);
});
+212
View File
@@ -0,0 +1,212 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';
import { assembleSessionDetail } from '../packages/core/src/session-detail.ts';
import { persist } from '../packages/core/src/persist.ts';
import { parse as parseCodex } from '../packages/core/src/providers/codex.ts';
const SCHEMA = readFileSync(new URL('../packages/core/src/schema.sql', import.meta.url), 'utf8');
function writeCodexFixture(lines) {
const dir = mkdtempSync(join(tmpdir(), 'obelisk-provider-detail-'));
const path = join(dir, 'rollout.jsonl');
writeFileSync(path, `${lines.map(line => JSON.stringify(line)).join('\n')}\n`);
return path;
}
test('a provider record stream assembles directly into session detail', () => {
const threadId = '019e8951-3e7d-7343-a3e3-05bff48a317d';
const path = writeCodexFixture([
{
type: 'session_meta',
timestamp: '2026-06-10T10:00:00Z',
payload: { id: threadId, cwd: '/proj', timestamp: '2026-06-10T10:00:00Z' },
},
{
type: 'event_msg',
timestamp: '2026-06-10T10:00:01Z',
payload: { type: 'user_message', message: 'inspect the repository' },
},
{
type: 'event_msg',
timestamp: '2026-06-10T10:00:02Z',
payload: { type: 'agent_message', message: 'I will inspect it.' },
},
{
type: 'response_item',
timestamp: '2026-06-10T10:00:03Z',
payload: { type: 'function_call', call_id: 'call_1', name: 'shell', arguments: '{"cmd":"ls"}' },
},
{
type: 'response_item',
timestamp: '2026-06-10T10:00:04Z',
payload: { type: 'function_call_output', call_id: 'call_1', output: 'package.json' },
},
]);
const records = [...parseCodex({ key: path, sessionId: '' }, null)];
const detail = assembleSessionDetail(records);
assert.deepEqual(detail.messages.map(message => message.text), [
'inspect the repository',
'I will inspect it.',
]);
assert.equal(detail.messages[1].tool_calls?.[0].name, 'shell');
assert.equal(detail.messages[1].tool_calls?.[0].result?.content, 'package.json');
const db = new DatabaseSync(':memory:');
db.exec(SCHEMA);
persist(db, { key: path, sessionId: '' }, parseCodex({ key: path, sessionId: '' }, null));
const persistedDetail = assembleSessionDetail({
session: db.prepare('SELECT * FROM sessions').get(),
messages: db.prepare('SELECT * FROM messages ORDER BY timestamp, uuid').all(),
toolCalls: db.prepare('SELECT * FROM tool_calls').all(),
toolResults: db.prepare('SELECT * FROM tool_results').all(),
});
assert.deepEqual(persistedDetail, detail);
db.close();
});
test('provider-classified hidden context never reaches session detail', () => {
const threadId = '019e8951-3e7d-7343-a3e3-05bff48a317e';
const path = writeCodexFixture([
{
type: 'session_meta',
timestamp: '2026-06-10T10:00:00Z',
payload: { id: threadId, cwd: '/proj', timestamp: '2026-06-10T10:00:00Z' },
},
{
type: 'response_item',
timestamp: '2026-06-10T10:00:01Z',
payload: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: '<environment_context>\n <cwd>/proj</cwd>\n</environment_context>' }],
},
},
{
type: 'response_item',
timestamp: '2026-06-10T10:00:02Z',
payload: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: '<codex_internal_context source="goal">\nsecret state\n</codex_internal_context>' }],
},
},
{
type: 'event_msg',
timestamp: '2026-06-10T10:00:03Z',
payload: { type: 'user_message', message: 'show the actual request' },
},
]);
const records = [...parseCodex({ key: path, sessionId: '' }, null)];
const detail = assembleSessionDetail(records);
assert.deepEqual(detail.messages.map(message => message.text), ['show the actual request']);
assert.equal(
records.filter(record => record.kind === 'message' && record.visibility === 'hidden').length,
2,
);
});
test('provider normalization removes only structural image wrappers before deduplication', () => {
const threadId = '019e8951-3e7d-7343-a3e3-05bff48a317f';
const path = writeCodexFixture([
{
type: 'session_meta',
timestamp: '2026-06-10T10:00:00Z',
payload: { id: threadId, cwd: '/proj', timestamp: '2026-06-10T10:00:00Z' },
},
{
type: 'event_msg',
timestamp: '2026-06-10T10:00:01Z',
payload: { type: 'user_message', message: 'look at this screenshot' },
},
{
type: 'response_item',
timestamp: '2026-06-10T10:00:01Z',
payload: {
type: 'message',
role: 'user',
content: [
{ type: 'input_text', text: 'look at this screenshot' },
{ type: 'input_text', text: '<image>' },
{ type: 'input_image', image_url: 'data:image/png;base64,AAAA' },
{ type: 'input_text', text: '</image>' },
],
},
},
]);
const records = [...parseCodex({ key: path, sessionId: '' }, null)];
const detail = assembleSessionDetail(records);
assert.deepEqual(detail.messages.map(message => message.text), ['look at this screenshot']);
});
test('canonical visibility survives persistence before row-based assembly', () => {
const threadId = '019e8951-3e7d-7343-a3e3-05bff48a3180';
const path = writeCodexFixture([
{
type: 'session_meta',
timestamp: '2026-06-10T10:00:00Z',
payload: { id: threadId, cwd: '/proj', timestamp: '2026-06-10T10:00:00Z' },
},
{
type: 'response_item',
timestamp: '2026-06-10T10:00:01Z',
payload: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: '<environment_context>hidden</environment_context>' }],
},
},
{
type: 'event_msg',
timestamp: '2026-06-10T10:00:02Z',
payload: { type: 'user_message', message: 'visible request' },
},
]);
const db = new DatabaseSync(':memory:');
db.exec(SCHEMA);
persist(db, { key: path, sessionId: '' }, parseCodex({ key: path, sessionId: '' }, null));
const messages = db.prepare('SELECT * FROM messages ORDER BY timestamp, uuid').all();
const assembled = assembleSessionDetail({ messages }).messages;
assert.equal(messages[0].visibility, 'hidden');
assert.deepEqual(assembled.map(message => message.text), ['visible request']);
db.close();
});
test('provider normalization classifies Skill instructions before assembly', () => {
const threadId = '019e8951-3e7d-7343-a3e3-05bff48a3181';
const path = writeCodexFixture([
{
type: 'session_meta',
timestamp: '2026-06-10T10:00:00Z',
payload: { id: threadId, cwd: '/proj', timestamp: '2026-06-10T10:00:00Z' },
},
{
type: 'response_item',
timestamp: '2026-06-10T10:00:01Z',
payload: {
type: 'message',
role: 'user',
content: [{ type: 'input_text', text: 'Base directory for this skill: /tmp/skill\n# Instructions' }],
},
},
]);
const records = [...parseCodex({ key: path, sessionId: '' }, null)];
const message = records.find(record => record.kind === 'message');
assert.equal(message.content_type, 'skill_instructions');
assert.equal(message.is_meta, 1);
assert.equal(message.visibility, 'visible');
});
+3 -3
View File
@@ -173,8 +173,8 @@ test('runtime indexes Codex root sessions into the shared query helpers', () =>
})),
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],
tool: sql('SELECT id, message_uuid, session_id, name FROM tool_calls WHERE id=?', ${JSON.stringify(`codex:${codexId}:call_codex_1`)})[0],
toolResult: sql('SELECT tool_use_id, message_uuid, session_id, content FROM tool_results WHERE tool_use_id=?', ${JSON.stringify(`codex:${codexId}:call_codex_1`)})[0],
overviewSources: overview({ limit: 5 }).totals.sources
};
`);
@@ -440,7 +440,7 @@ test('runtime maps Codex child threads onto subagents', () => {
assert.deepEqual(payload.subagents, [{
agent_id: `codex:${childId}`,
session_id: `codex:${parentId}`,
parent_tool_use_id: 'codex:call_spawn_1',
parent_tool_use_id: `codex:${parentId}:call_spawn_1`,
agent_type: 'worker',
description: 'Plato',
messageCount: 2,
+70 -16
View File
@@ -1,21 +1,33 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { assembleSessionMessages } from '../app/src/shared/session-detail-assembly.mjs';
import { assembleSessionDetail } from '../app/src/shared/session-detail-assembly.mjs';
test('session assembly preserves thinking and attaches tool result and subagent evidence', () => {
const messages = [
{ uuid: 'thinking-1', type: 'assistant', content_type: 'thinking', text: 'reasoning' },
{ uuid: 'answer-1', type: 'assistant', content_type: 'text', text: 'answer' },
{ uuid: 'tool-1', type: 'assistant', content_type: 'tool_use', text: '' },
{ uuid: 'result-1', type: 'user', content_type: 'tool_result', text: '' },
{
uuid: 'thinking-1', timestamp: '2026-06-10T10:00:00Z',
type: 'assistant', content_type: 'thinking', text: 'reasoning',
},
{
uuid: 'answer-1', timestamp: '2026-06-10T10:00:01Z',
type: 'assistant', content_type: 'text', text: 'answer',
},
{
uuid: 'tool-1', timestamp: '2026-06-10T10:00:02Z',
type: 'assistant', content_type: 'tool_use', text: '',
},
{
uuid: 'result-1', timestamp: '2026-06-10T10:00:03Z',
type: 'user', content_type: 'tool_result', text: '',
},
];
const assembled = assembleSessionMessages({
const assembled = assembleSessionDetail({
messages,
toolCalls: [{ id: 'call-1', message_uuid: 'tool-1', name: 'Agent', input_json: '{"description":"inspect"}' }],
toolResults: [{ tool_use_id: 'call-1', message_uuid: 'result-1', content: 'done', is_error: 0 }],
subagents: [{ agent_id: 'agent-1', parent_tool_use_id: 'call-1', agent_type: 'reviewer', description: 'inspect' }],
workflows: [],
});
}).messages;
assert.equal(assembled.length, 1);
assert.equal(assembled[0].uuid, 'answer-1');
@@ -25,34 +37,76 @@ test('session assembly preserves thinking and attaches tool result and subagent
});
test('session assembly keeps Skill evidence standalone and embeds matching workflow agents', () => {
const assembled = assembleSessionMessages({
const assembled = assembleSessionDetail({
messages: [
{ uuid: 'skill-1', type: 'assistant', content_type: 'tool_use', text: '' },
{ uuid: 'skill-md', type: 'user', content_type: 'text', is_meta: 1, text: 'Base directory for this skill\n# Skill' },
{ uuid: 'skill-md', type: 'user', content_type: 'skill_instructions', is_meta: 1, text: '# Skill instructions' },
{ uuid: 'workflow-1', type: 'assistant', content_type: 'tool_use', text: '' },
],
toolCalls: [
{ id: 'call-skill', message_uuid: 'skill-1', name: 'Skill', input_json: '{"skill":"obelisk"}' },
{ id: 'call-workflow', message_uuid: 'workflow-1', name: 'Workflow', input_json: '{}' },
{ id: 'call-skill', message_uuid: 'skill-1', name: 'Skill', presentation: 'skill', input_json: '{"skill":"obelisk"}' },
{ id: 'call-workflow', message_uuid: 'workflow-1', name: 'Workflow', presentation: 'default', input_json: '{}' },
],
toolResults: [{ tool_use_id: 'call-workflow', content: 'run-1 complete', is_error: 0 }],
toolResults: [{ tool_use_id: 'call-workflow', content: 'complete', is_error: 0 }],
subagents: [],
workflows: [{
run_id: 'run-1',
parent_tool_use_id: 'call-workflow',
workflow_name: 'review',
status: 'complete',
agents: [{ agent_id: 'agent-1', phase: 'review', label: 'Reviewer', state: 'complete' }],
}],
});
}).messages;
assert.equal(assembled[0]._skillMd, 'Base directory for this skill\n# Skill');
assert.equal(assembled[0]._skillMd, '# Skill instructions');
assert.equal(assembled[1].tool_calls[0].workflow.run_id, 'run-1');
assert.deepEqual(assembled[1].tool_calls[0].workflow.agents, [{
agent_id: 'agent-1',
phase: 'review',
label: 'Reviewer',
state: 'complete',
tokens: undefined,
duration_ms: undefined,
tokens: null,
duration_ms: null,
}]);
});
test('session assembly trusts canonical classification instead of parsing provider text', () => {
const detail = assembleSessionDetail({
messages: [{
uuid: 'provider-owned-classification',
type: 'user',
content_type: 'text',
is_meta: 0,
text: '<system-reminder>text alone does not define presentation semantics</system-reminder>',
}],
});
assert.equal(detail.messages[0].is_meta, 0);
});
test('canonical ordering is stable across provider and SQLite iteration order', () => {
const detail = assembleSessionDetail([
{
kind: 'message', uuid: 'b', session_id: 'session', type: 'user', parent_uuid: null,
timestamp: '2026-06-10T10:00:00Z', role: 'user', text: 'second', content_type: 'text',
is_meta: 0, visibility: 'visible', model: null, is_sidechain: 0, agent_id: null,
input_tokens: null, output_tokens: null, cwd: null, skill: null, source: 'test',
},
{
kind: 'message', uuid: 'a', session_id: 'session', type: 'user', parent_uuid: null,
timestamp: '2026-06-10T10:00:00Z', role: 'user', text: 'first', content_type: 'text',
is_meta: 0, visibility: 'visible', model: null, is_sidechain: 0, agent_id: null,
input_tokens: null, output_tokens: null, cwd: null, skill: null, source: 'test',
},
]);
assert.deepEqual(detail.messages.map(message => message.uuid), ['a', 'b']);
});
test('direct session assembly rejects an incomplete provider delta', () => {
assert.throws(() => assembleSessionDetail([{
kind: 'session', id: 'session', title: null, project: null,
started_at: null, ended_at: null, git_branch: null, version: null,
message_count: 1, countMode: 'delta', jsonl_path: '/session.jsonl', source: 'test',
}]), /fresh full parse/);
});
+3 -3
View File
@@ -9,7 +9,7 @@ import {
loadSessionDetail,
materializeSessionDetailPatch,
} from '../app/src/renderer/src/data.js';
import { assembleSessionMessages } from '../app/src/shared/session-detail-assembly.mjs';
import { assembleSessionDetail } from '../app/src/shared/session-detail-assembly.mjs';
import { createSessionPatch } from '../app/src/shared/session-patch.mjs';
test('live updates coalesce while scrolling and load only the latest after scroll end', async () => {
@@ -123,13 +123,13 @@ test('a skipped live patch does not advance the visible patch baseline', async t
getSessionWorkflows: async () => [],
getSessionSummaries: async () => [],
getSessionPatch: async (_id, cursor) => {
const snapshotAtCall = { messages: assembleSessionMessages({
const snapshotAtCall = { messages: assembleSessionDetail({
messages: rows,
toolCalls: [],
toolResults: [],
subagents: [],
workflows: [],
}), workflows: [] };
}).messages, workflows: [] };
patchCalls++;
if (patchCalls === 1) {
firstPatchStarted();