2026-07-21 00:58:34 +08:00
|
|
|
import type {
|
|
|
|
|
TranscriptRecord,
|
|
|
|
|
MessageRecord,
|
|
|
|
|
SessionRecord,
|
|
|
|
|
SummaryRecord,
|
|
|
|
|
ToolCallRecord,
|
|
|
|
|
ToolResultRecord,
|
|
|
|
|
WorkflowAgentRecord,
|
|
|
|
|
WorkflowRecord,
|
|
|
|
|
} from './providers/types.ts';
|
|
|
|
|
|
|
|
|
|
type WithoutKind<T extends { kind: string }> = Omit<T, 'kind'>;
|
|
|
|
|
|
|
|
|
|
export interface SessionDetailMessage {
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
uuid: string;
|
|
|
|
|
type: string | null;
|
|
|
|
|
timestamp: string | null;
|
|
|
|
|
text: string | null;
|
|
|
|
|
content_type: string | null;
|
|
|
|
|
is_meta: 0 | 1;
|
2026-07-31 03:11:55 +08:00
|
|
|
session_id: string | null;
|
|
|
|
|
cwd: string | null;
|
2026-07-21 00:58:34 +08:00
|
|
|
turn_duration_ms: number | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type SessionDetailToolResult = WithoutKind<ToolResultRecord>;
|
|
|
|
|
|
|
|
|
|
export interface SessionDetailWorkflowAgent {
|
|
|
|
|
agent_id: string;
|
|
|
|
|
phase: string | null;
|
|
|
|
|
label: string | null;
|
|
|
|
|
state: string | null;
|
|
|
|
|
tokens: number | null;
|
|
|
|
|
duration_ms: number | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SessionDetailWorkflow {
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
run_id: string;
|
|
|
|
|
parent_tool_use_id: string | null;
|
|
|
|
|
workflow_name: string | null;
|
|
|
|
|
status: string | null;
|
|
|
|
|
duration_ms: number | null;
|
|
|
|
|
total_tokens: number | null;
|
|
|
|
|
agent_count: number | null;
|
|
|
|
|
agents: SessionDetailWorkflowAgent[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface AssembledToolCall {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
presentation: 'default' | 'skill';
|
|
|
|
|
input_json: string | null;
|
|
|
|
|
result: SessionDetailToolResult | null;
|
|
|
|
|
subagent?: {
|
|
|
|
|
agent_id: string;
|
|
|
|
|
agent_type: string | null;
|
|
|
|
|
description: string | null;
|
|
|
|
|
};
|
|
|
|
|
workflow?: SessionDetailWorkflow;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface AssembledMessage extends SessionDetailMessage {
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
tool_calls?: AssembledToolCall[];
|
|
|
|
|
_thinking?: string;
|
|
|
|
|
_skillMd?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SessionDetailSnapshot {
|
|
|
|
|
session: SessionDetailSession | null;
|
|
|
|
|
messages: AssembledMessage[];
|
|
|
|
|
workflows: SessionDetailWorkflow[];
|
|
|
|
|
summaries: SessionDetailSummary[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type SessionDetailSummary = WithoutKind<SummaryRecord> & Record<string, unknown>;
|
|
|
|
|
|
|
|
|
|
export type SessionDetailSession = Omit<WithoutKind<SessionRecord>, 'countMode'>;
|
|
|
|
|
|
|
|
|
|
export interface SessionDetailSessionRow {
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
id: string;
|
|
|
|
|
title?: string | null;
|
|
|
|
|
project?: string | null;
|
|
|
|
|
started_at?: string | null;
|
|
|
|
|
ended_at?: string | null;
|
|
|
|
|
git_branch?: string | null;
|
|
|
|
|
version?: string | null;
|
|
|
|
|
message_count?: number | null;
|
|
|
|
|
jsonl_path?: string | null;
|
|
|
|
|
source?: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SessionMessageRow {
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
uuid: string;
|
|
|
|
|
session_id?: string | null;
|
|
|
|
|
type?: string | null;
|
|
|
|
|
role?: string | null;
|
|
|
|
|
parent_uuid?: string | null;
|
|
|
|
|
timestamp?: string | null;
|
|
|
|
|
text?: string | null;
|
|
|
|
|
content_type?: string | null;
|
|
|
|
|
is_meta?: number | boolean | null;
|
|
|
|
|
visibility?: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SessionToolResultRow {
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
tool_use_id: string;
|
|
|
|
|
message_uuid?: string | null;
|
|
|
|
|
session_id?: string | null;
|
|
|
|
|
content?: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SessionToolCallRow {
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
id: string;
|
|
|
|
|
message_uuid: string;
|
|
|
|
|
session_id?: string | null;
|
|
|
|
|
name: string;
|
|
|
|
|
presentation?: string | null;
|
|
|
|
|
input_json?: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SessionSubagentRow {
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
agent_id: string;
|
|
|
|
|
session_id?: string | null;
|
|
|
|
|
parent_tool_use_id?: string | null;
|
|
|
|
|
agent_type?: string | null;
|
|
|
|
|
description?: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SessionWorkflowAgentRow {
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
agent_id: string;
|
|
|
|
|
run_id?: string | null;
|
|
|
|
|
session_id?: string | null;
|
|
|
|
|
phase?: string | null;
|
|
|
|
|
label?: string | null;
|
|
|
|
|
state?: string | null;
|
|
|
|
|
tokens?: number | null;
|
|
|
|
|
duration_ms?: number | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SessionWorkflowRow {
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
run_id: string;
|
|
|
|
|
session_id?: string | null;
|
|
|
|
|
parent_tool_use_id?: string | null;
|
|
|
|
|
workflow_name?: string | null;
|
|
|
|
|
status?: string | null;
|
|
|
|
|
duration_ms?: number | null;
|
|
|
|
|
total_tokens?: number | null;
|
|
|
|
|
agent_count?: number | null;
|
|
|
|
|
agents?: SessionWorkflowAgentRow[] | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SessionSummaryRow {
|
|
|
|
|
[key: string]: unknown;
|
|
|
|
|
id: string | number;
|
|
|
|
|
session_id?: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SessionDetailRows {
|
|
|
|
|
session?: SessionDetailSessionRow | null;
|
|
|
|
|
messages?: SessionMessageRow[];
|
|
|
|
|
toolCalls?: SessionToolCallRow[];
|
|
|
|
|
toolResults?: SessionToolResultRow[];
|
|
|
|
|
subagents?: SessionSubagentRow[];
|
|
|
|
|
workflows?: SessionWorkflowRow[];
|
|
|
|
|
summaries?: SessionSummaryRow[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function withoutKind<T extends { kind: string }>(record: T): WithoutKind<T> {
|
|
|
|
|
const { kind: _kind, ...value } = record;
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function assembleMessages(
|
|
|
|
|
messages: SessionDetailMessage[],
|
|
|
|
|
toolCalls: ToolCallRecord[],
|
|
|
|
|
toolResults: ToolResultRecord[],
|
|
|
|
|
subagents: Extract<TranscriptRecord, { kind: 'subagent' }>[],
|
|
|
|
|
workflows: SessionDetailWorkflow[],
|
|
|
|
|
): AssembledMessage[] {
|
|
|
|
|
const resultsByCallId = new Map<string, SessionDetailToolResult>();
|
|
|
|
|
for (const result of toolResults) resultsByCallId.set(result.tool_use_id, withoutKind(result));
|
|
|
|
|
|
|
|
|
|
const subagentsByCallId = new Map<string, Extract<TranscriptRecord, { kind: 'subagent' }>>();
|
|
|
|
|
for (const subagent of subagents) {
|
|
|
|
|
if (subagent.parent_tool_use_id) subagentsByCallId.set(subagent.parent_tool_use_id, subagent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const callsByMessageUuid = new Map<string, AssembledToolCall[]>();
|
|
|
|
|
const workflowsByCallId = new Map(
|
|
|
|
|
workflows
|
|
|
|
|
.filter((workflow) => workflow.parent_tool_use_id)
|
|
|
|
|
.map((workflow) => [workflow.parent_tool_use_id as string, workflow]),
|
|
|
|
|
);
|
|
|
|
|
for (const toolCall of toolCalls) {
|
|
|
|
|
const call: AssembledToolCall = {
|
|
|
|
|
id: toolCall.id,
|
|
|
|
|
name: toolCall.name,
|
|
|
|
|
presentation: toolCall.presentation,
|
|
|
|
|
input_json: toolCall.input_json,
|
|
|
|
|
result: resultsByCallId.get(toolCall.id) ?? null,
|
|
|
|
|
};
|
|
|
|
|
const subagent = subagentsByCallId.get(toolCall.id);
|
|
|
|
|
if (subagent) {
|
|
|
|
|
call.subagent = {
|
|
|
|
|
agent_id: subagent.agent_id,
|
|
|
|
|
agent_type: subagent.agent_type ?? null,
|
|
|
|
|
description: subagent.description ?? null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const workflow = workflowsByCallId.get(toolCall.id);
|
|
|
|
|
if (workflow) call.workflow = workflow;
|
|
|
|
|
const calls = callsByMessageUuid.get(toolCall.message_uuid) ?? [];
|
|
|
|
|
calls.push(call);
|
|
|
|
|
callsByMessageUuid.set(toolCall.message_uuid, calls);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const raw = messages.map((message): AssembledMessage => {
|
|
|
|
|
const assembled: AssembledMessage = { ...message };
|
|
|
|
|
const calls = callsByMessageUuid.get(message.uuid);
|
|
|
|
|
if (calls?.length) assembled.tool_calls = calls;
|
|
|
|
|
return assembled;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const output: AssembledMessage[] = [];
|
|
|
|
|
for (let index = 0; index < raw.length; index++) {
|
|
|
|
|
const message = raw[index];
|
|
|
|
|
if (message.content_type === 'tool_result') continue;
|
|
|
|
|
|
|
|
|
|
if (message.type === 'assistant' && message.content_type === 'thinking') {
|
|
|
|
|
const thinkingParts = [message.text ?? ''];
|
|
|
|
|
let nextIndex = index + 1;
|
|
|
|
|
while (
|
|
|
|
|
nextIndex < raw.length
|
|
|
|
|
&& raw[nextIndex].type === 'assistant'
|
|
|
|
|
&& raw[nextIndex].content_type === 'thinking'
|
|
|
|
|
) {
|
|
|
|
|
thinkingParts.push(raw[nextIndex].text ?? '');
|
|
|
|
|
nextIndex++;
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
nextIndex < raw.length
|
|
|
|
|
&& raw[nextIndex].type === 'assistant'
|
|
|
|
|
&& raw[nextIndex].content_type !== 'thinking'
|
|
|
|
|
) {
|
|
|
|
|
raw[nextIndex]._thinking = thinkingParts.join('\n\n');
|
|
|
|
|
index = nextIndex - 1;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
output.push({ ...message, text: thinkingParts.join('\n\n'), content_type: 'thinking' });
|
|
|
|
|
index = nextIndex - 1;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (message.type === 'assistant' && message.content_type === 'tool_use') {
|
|
|
|
|
const merged: AssembledMessage = {
|
|
|
|
|
...message,
|
|
|
|
|
tool_calls: [...(message.tool_calls ?? [])],
|
|
|
|
|
};
|
|
|
|
|
const mergedCalls = merged.tool_calls ?? [];
|
|
|
|
|
if (message._thinking) merged._thinking = message._thinking;
|
|
|
|
|
const skillOnly = mergedCalls.length === 1 && mergedCalls[0].presentation === 'skill';
|
|
|
|
|
let nextIndex = index + 1;
|
|
|
|
|
while (nextIndex < raw.length) {
|
|
|
|
|
const next = raw[nextIndex];
|
|
|
|
|
if (next.content_type === 'tool_result') {
|
|
|
|
|
nextIndex++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (next.content_type === 'skill_instructions' && next.text) {
|
|
|
|
|
merged._skillMd = next.text;
|
|
|
|
|
nextIndex++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!skillOnly && next.type === 'assistant' && next.content_type === 'tool_use') {
|
|
|
|
|
if (next.tool_calls) mergedCalls.push(...next.tool_calls);
|
|
|
|
|
if (next.text && !merged.text) merged.text = next.text;
|
|
|
|
|
nextIndex++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
output.push(merged);
|
|
|
|
|
index = nextIndex - 1;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const assembled: AssembledMessage = { ...message };
|
|
|
|
|
if (message._thinking) assembled._thinking = message._thinking;
|
|
|
|
|
if (message.type === 'assistant'
|
|
|
|
|
&& message.content_type !== 'tool_use'
|
|
|
|
|
&& message.content_type !== 'thinking') {
|
|
|
|
|
if (!assembled.tool_calls) assembled.tool_calls = [];
|
|
|
|
|
let nextIndex = index + 1;
|
|
|
|
|
while (nextIndex < raw.length) {
|
|
|
|
|
const next = raw[nextIndex];
|
|
|
|
|
if (next.content_type === 'tool_result') {
|
|
|
|
|
nextIndex++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (next.type === 'assistant' && next.content_type === 'tool_use') {
|
|
|
|
|
if (next.tool_calls) assembled.tool_calls.push(...next.tool_calls);
|
|
|
|
|
nextIndex++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (!assembled.tool_calls.length) delete assembled.tool_calls;
|
|
|
|
|
index = nextIndex - 1;
|
|
|
|
|
}
|
|
|
|
|
output.push(assembled);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return output;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Project one canonical provider record stream into the app's session detail.
|
|
|
|
|
* Provider-specific wire semantics must already be resolved before this seam.
|
|
|
|
|
*/
|
|
|
|
|
function assembleTranscriptRecords(records: Iterable<TranscriptRecord>): SessionDetailSnapshot {
|
|
|
|
|
let session: SessionDetailSession | null = null;
|
|
|
|
|
const messages: SessionDetailMessage[] = [];
|
|
|
|
|
const messagesByUuid = new Map<string, SessionDetailMessage>();
|
|
|
|
|
const mainMessageUuids = new Set<string>();
|
|
|
|
|
const toolCalls: ToolCallRecord[] = [];
|
|
|
|
|
const toolResults: ToolResultRecord[] = [];
|
|
|
|
|
const subagents: Extract<TranscriptRecord, { kind: 'subagent' }>[] = [];
|
|
|
|
|
const workflows: WorkflowRecord[] = [];
|
|
|
|
|
const workflowAgentsById = new Map<string, WorkflowAgentRecord>();
|
|
|
|
|
const summaries: SessionDetailSummary[] = [];
|
|
|
|
|
|
|
|
|
|
for (const record of records) {
|
|
|
|
|
switch (record.kind) {
|
|
|
|
|
case 'session':
|
|
|
|
|
if (record.countMode === 'delta') {
|
|
|
|
|
throw new Error('Direct session detail assembly requires a fresh full parse (cursor = null), not a provider delta');
|
|
|
|
|
}
|
|
|
|
|
session = {
|
|
|
|
|
id: record.id,
|
|
|
|
|
title: record.title,
|
|
|
|
|
project: record.project,
|
|
|
|
|
started_at: record.started_at,
|
|
|
|
|
ended_at: record.ended_at,
|
|
|
|
|
git_branch: record.git_branch,
|
|
|
|
|
version: record.version,
|
|
|
|
|
message_count: record.message_count,
|
|
|
|
|
jsonl_path: record.jsonl_path,
|
|
|
|
|
source: record.source,
|
|
|
|
|
};
|
|
|
|
|
break;
|
|
|
|
|
case 'message': {
|
|
|
|
|
if (record.visibility === 'hidden') break;
|
|
|
|
|
const message: SessionDetailMessage = {
|
|
|
|
|
uuid: record.uuid,
|
|
|
|
|
type: record.type || record.role,
|
|
|
|
|
timestamp: record.timestamp,
|
|
|
|
|
text: record.text,
|
|
|
|
|
content_type: record.content_type,
|
|
|
|
|
is_meta: record.is_meta,
|
2026-07-31 03:11:55 +08:00
|
|
|
// Both kept per message, not per session: the working directory can change
|
|
|
|
|
// mid-session, and the session id scopes which roots a file reference may resolve in.
|
|
|
|
|
session_id: typeof record.session_id === 'string' ? record.session_id : null,
|
|
|
|
|
cwd: typeof record.cwd === 'string' ? record.cwd : null,
|
2026-07-21 00:58:34 +08:00
|
|
|
turn_duration_ms: typeof (record as MessageRecord & { turn_duration_ms?: unknown }).turn_duration_ms === 'number'
|
|
|
|
|
? (record as MessageRecord & { turn_duration_ms: number }).turn_duration_ms
|
|
|
|
|
: null,
|
|
|
|
|
};
|
|
|
|
|
messages.push(message);
|
|
|
|
|
messagesByUuid.set(message.uuid, message);
|
|
|
|
|
if (record.agent_id === null) mainMessageUuids.add(message.uuid);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case 'tool_call':
|
|
|
|
|
toolCalls.push(record);
|
|
|
|
|
break;
|
|
|
|
|
case 'tool_result':
|
|
|
|
|
toolResults.push(record);
|
|
|
|
|
break;
|
|
|
|
|
case 'subagent':
|
|
|
|
|
subagents.push(record);
|
|
|
|
|
break;
|
|
|
|
|
case 'workflow':
|
|
|
|
|
workflows.push(record);
|
|
|
|
|
break;
|
|
|
|
|
case 'workflow_agent':
|
|
|
|
|
workflowAgentsById.set(record.agent_id, {
|
|
|
|
|
...(workflowAgentsById.get(record.agent_id) ?? record),
|
|
|
|
|
...Object.fromEntries(
|
|
|
|
|
Object.entries(record).filter(([, value]) => value !== null && value !== undefined),
|
|
|
|
|
),
|
|
|
|
|
} as WorkflowAgentRecord);
|
|
|
|
|
break;
|
|
|
|
|
case 'summary':
|
|
|
|
|
summaries.push(withoutKind(record));
|
|
|
|
|
break;
|
|
|
|
|
case 'message-turn-duration': {
|
|
|
|
|
const message = messagesByUuid.get(record.uuid);
|
|
|
|
|
if (message) message.turn_duration_ms = record.turn_duration_ms;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case 'delete-session':
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const assembledWorkflows: SessionDetailWorkflow[] = workflows.map((workflow) => {
|
|
|
|
|
const agents = [...workflowAgentsById.values()]
|
|
|
|
|
.filter((agent) => agent.run_id === workflow.run_id)
|
|
|
|
|
.map((agent) => ({
|
|
|
|
|
agent_id: agent.agent_id,
|
|
|
|
|
phase: agent.phase ?? null,
|
|
|
|
|
label: agent.label ?? null,
|
|
|
|
|
state: agent.state ?? null,
|
|
|
|
|
tokens: agent.tokens ?? null,
|
|
|
|
|
duration_ms: agent.duration_ms ?? null,
|
|
|
|
|
}));
|
|
|
|
|
return {
|
|
|
|
|
run_id: workflow.run_id,
|
|
|
|
|
parent_tool_use_id: workflow.parent_tool_use_id ?? null,
|
|
|
|
|
workflow_name: workflow.workflow_name,
|
|
|
|
|
status: workflow.status,
|
|
|
|
|
duration_ms: workflow.duration_ms,
|
|
|
|
|
total_tokens: workflow.total_tokens,
|
|
|
|
|
agent_count: workflow.agent_count ?? agents.length,
|
|
|
|
|
agents,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const detailMessages = session === null
|
|
|
|
|
? messages
|
|
|
|
|
: messages.filter((message) => mainMessageUuids.has(message.uuid));
|
|
|
|
|
detailMessages.sort((left, right) => {
|
|
|
|
|
const leftTimestamp = left.timestamp ?? '';
|
|
|
|
|
const rightTimestamp = right.timestamp ?? '';
|
|
|
|
|
if (leftTimestamp !== rightTimestamp) return leftTimestamp < rightTimestamp ? -1 : 1;
|
|
|
|
|
return left.uuid < right.uuid ? -1 : left.uuid > right.uuid ? 1 : 0;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
session,
|
|
|
|
|
messages: assembleMessages(detailMessages, toolCalls, toolResults, subagents, assembledWorkflows),
|
|
|
|
|
workflows: assembledWorkflows,
|
|
|
|
|
summaries,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Adapt persisted rows back into the same canonical record language providers emit. */
|
|
|
|
|
function sessionDetailRecordsFromRows(input: SessionDetailRows): TranscriptRecord[] {
|
|
|
|
|
const records: TranscriptRecord[] = [];
|
|
|
|
|
if (input.session) {
|
|
|
|
|
const session = input.session;
|
|
|
|
|
records.push({
|
|
|
|
|
kind: 'session',
|
|
|
|
|
id: session.id,
|
|
|
|
|
title: typeof session.title === 'string' ? session.title : null,
|
|
|
|
|
project: typeof session.project === 'string' ? session.project : null,
|
|
|
|
|
started_at: typeof session.started_at === 'string' ? session.started_at : null,
|
|
|
|
|
ended_at: typeof session.ended_at === 'string' ? session.ended_at : null,
|
|
|
|
|
git_branch: typeof session.git_branch === 'string' ? session.git_branch : null,
|
|
|
|
|
version: typeof session.version === 'string' ? session.version : null,
|
|
|
|
|
message_count: typeof session.message_count === 'number' ? session.message_count : 0,
|
|
|
|
|
countMode: 'total',
|
|
|
|
|
jsonl_path: typeof session.jsonl_path === 'string' ? session.jsonl_path : '',
|
|
|
|
|
source: typeof session.source === 'string' ? session.source : '',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
for (const message of input.messages ?? []) {
|
|
|
|
|
records.push({
|
|
|
|
|
...message,
|
|
|
|
|
kind: 'message',
|
|
|
|
|
uuid: message.uuid,
|
|
|
|
|
session_id: typeof message.session_id === 'string' ? message.session_id : '',
|
|
|
|
|
type: typeof message.type === 'string' ? message.type : typeof message.role === 'string' ? message.role : '',
|
|
|
|
|
parent_uuid: typeof message.parent_uuid === 'string' ? message.parent_uuid : null,
|
|
|
|
|
timestamp: typeof message.timestamp === 'string' ? message.timestamp : null,
|
|
|
|
|
role: typeof message.role === 'string' ? message.role : null,
|
|
|
|
|
text: typeof message.text === 'string' ? message.text : null,
|
|
|
|
|
content_type: typeof message.content_type === 'string' ? message.content_type : null,
|
|
|
|
|
is_meta: message.is_meta ? 1 : 0,
|
|
|
|
|
visibility: message.visibility === 'hidden' ? 'hidden' : 'visible',
|
|
|
|
|
model: typeof message.model === 'string' ? message.model : null,
|
|
|
|
|
is_sidechain: message.is_sidechain ? 1 : 0,
|
|
|
|
|
agent_id: typeof message.agent_id === 'string' ? message.agent_id : null,
|
|
|
|
|
input_tokens: typeof message.input_tokens === 'number' ? message.input_tokens : null,
|
|
|
|
|
output_tokens: typeof message.output_tokens === 'number' ? message.output_tokens : null,
|
|
|
|
|
cwd: typeof message.cwd === 'string' ? message.cwd : null,
|
|
|
|
|
skill: typeof message.skill === 'string' ? message.skill : null,
|
|
|
|
|
source: typeof message.source === 'string' ? message.source : '',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
for (const toolCall of input.toolCalls ?? []) {
|
|
|
|
|
records.push({
|
|
|
|
|
...toolCall,
|
|
|
|
|
kind: 'tool_call',
|
|
|
|
|
id: toolCall.id,
|
|
|
|
|
message_uuid: toolCall.message_uuid,
|
|
|
|
|
session_id: typeof toolCall.session_id === 'string' ? toolCall.session_id : '',
|
|
|
|
|
name: toolCall.name,
|
|
|
|
|
presentation: toolCall.presentation === 'skill' ? 'skill' : 'default',
|
|
|
|
|
input_json: typeof toolCall.input_json === 'string' ? toolCall.input_json : '',
|
|
|
|
|
file_path: typeof toolCall.file_path === 'string' ? toolCall.file_path : null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
for (const result of input.toolResults ?? []) {
|
|
|
|
|
records.push({
|
|
|
|
|
...result,
|
|
|
|
|
kind: 'tool_result',
|
|
|
|
|
tool_use_id: result.tool_use_id,
|
|
|
|
|
message_uuid: typeof result.message_uuid === 'string' ? result.message_uuid : '',
|
|
|
|
|
session_id: typeof result.session_id === 'string' ? result.session_id : '',
|
|
|
|
|
content: typeof result.content === 'string' ? result.content : '',
|
|
|
|
|
file_path: typeof result.file_path === 'string' ? result.file_path : null,
|
|
|
|
|
is_error: result.is_error ? 1 : 0,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
for (const subagent of input.subagents ?? []) {
|
|
|
|
|
records.push({
|
|
|
|
|
...subagent,
|
|
|
|
|
kind: 'subagent',
|
|
|
|
|
agent_id: subagent.agent_id,
|
|
|
|
|
session_id: typeof subagent.session_id === 'string' ? subagent.session_id : '',
|
|
|
|
|
parent_tool_use_id: typeof subagent.parent_tool_use_id === 'string' ? subagent.parent_tool_use_id : null,
|
|
|
|
|
agent_type: typeof subagent.agent_type === 'string' ? subagent.agent_type : null,
|
|
|
|
|
description: typeof subagent.description === 'string' ? subagent.description : null,
|
|
|
|
|
duration_ms: typeof subagent.duration_ms === 'number' ? subagent.duration_ms : null,
|
|
|
|
|
total_tokens: typeof subagent.total_tokens === 'number' ? subagent.total_tokens : null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
for (const workflow of input.workflows ?? []) {
|
|
|
|
|
records.push({
|
|
|
|
|
...workflow,
|
|
|
|
|
kind: 'workflow',
|
|
|
|
|
run_id: workflow.run_id,
|
|
|
|
|
session_id: typeof workflow.session_id === 'string' ? workflow.session_id : '',
|
|
|
|
|
parent_tool_use_id: typeof workflow.parent_tool_use_id === 'string' ? workflow.parent_tool_use_id : null,
|
|
|
|
|
task_id: typeof workflow.task_id === 'string' ? workflow.task_id : null,
|
|
|
|
|
script: typeof workflow.script === 'string' ? workflow.script : null,
|
|
|
|
|
result_json: typeof workflow.result_json === 'string' ? workflow.result_json : null,
|
|
|
|
|
timestamp: typeof workflow.timestamp === 'string' ? workflow.timestamp : null,
|
|
|
|
|
agent_count: typeof workflow.agent_count === 'number' ? workflow.agent_count : 0,
|
|
|
|
|
duration_ms: typeof workflow.duration_ms === 'number' ? workflow.duration_ms : null,
|
|
|
|
|
total_tokens: typeof workflow.total_tokens === 'number' ? workflow.total_tokens : null,
|
|
|
|
|
status: typeof workflow.status === 'string' ? workflow.status : null,
|
|
|
|
|
workflow_name: typeof workflow.workflow_name === 'string' ? workflow.workflow_name : null,
|
|
|
|
|
});
|
|
|
|
|
for (const agent of workflow.agents ?? []) {
|
|
|
|
|
records.push({
|
|
|
|
|
...agent,
|
|
|
|
|
kind: 'workflow_agent',
|
|
|
|
|
agent_id: agent.agent_id,
|
|
|
|
|
run_id: typeof agent.run_id === 'string' ? agent.run_id : workflow.run_id,
|
|
|
|
|
session_id: typeof agent.session_id === 'string'
|
|
|
|
|
? agent.session_id
|
|
|
|
|
: typeof workflow.session_id === 'string' ? workflow.session_id : '',
|
|
|
|
|
...(typeof agent.agent_type === 'string' ? { agent_type: agent.agent_type } : {}),
|
|
|
|
|
...(typeof agent.description === 'string' ? { description: agent.description } : {}),
|
|
|
|
|
...(typeof agent.phase === 'string' ? { phase: agent.phase } : {}),
|
|
|
|
|
...(typeof agent.label === 'string' ? { label: agent.label } : {}),
|
|
|
|
|
...(typeof agent.model === 'string' ? { model: agent.model } : {}),
|
|
|
|
|
...(typeof agent.state === 'string' ? { state: agent.state } : {}),
|
|
|
|
|
...(typeof agent.duration_ms === 'number' ? { duration_ms: agent.duration_ms } : {}),
|
|
|
|
|
...(typeof agent.tokens === 'number' ? { tokens: agent.tokens } : {}),
|
|
|
|
|
...(typeof agent.tool_calls === 'number' ? { tool_calls: agent.tool_calls } : {}),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for (const summary of input.summaries ?? []) {
|
|
|
|
|
records.push({
|
|
|
|
|
...summary,
|
|
|
|
|
kind: 'summary',
|
|
|
|
|
id: String(summary.id),
|
|
|
|
|
session_id: typeof summary.session_id === 'string' ? summary.session_id : '',
|
|
|
|
|
timestamp: typeof summary.timestamp === 'string' ? summary.timestamp : null,
|
|
|
|
|
source: typeof summary.source === 'string' ? summary.source : '',
|
|
|
|
|
content: typeof summary.content === 'string' ? summary.content : '',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return records;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Assemble detail from either a provider's complete canonical transcript stream
|
|
|
|
|
* (a fresh parse with cursor = null) or the same records after a persistence
|
|
|
|
|
* round-trip. This is the only presentation seam.
|
|
|
|
|
*/
|
|
|
|
|
export function assembleSessionDetail(
|
|
|
|
|
input: Iterable<TranscriptRecord> | SessionDetailRows,
|
|
|
|
|
): SessionDetailSnapshot {
|
|
|
|
|
const records = Symbol.iterator in input
|
|
|
|
|
? input as Iterable<TranscriptRecord>
|
|
|
|
|
: sessionDetailRecordsFromRows(input as SessionDetailRows);
|
|
|
|
|
return assembleTranscriptRecords(records);
|
|
|
|
|
}
|