Files
hapi/cli/src/codex/utils/appServerEventConverter.ts
T

1213 lines
46 KiB
TypeScript

import { logger } from '@/ui/logger';
type ConvertedEvent = {
type: string;
[key: string]: unknown;
};
function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== 'object') {
return null;
}
return value as Record<string, unknown>;
}
function asString(value: unknown): string | null {
return typeof value === 'string' && value.length > 0 ? value : null;
}
function asBoolean(value: unknown): boolean | null {
return typeof value === 'boolean' ? value : null;
}
function asNumber(value: unknown): number | null {
return typeof value === 'number' && Number.isFinite(value) ? value : null;
}
function extractItemId(params: Record<string, unknown>): string | null {
const direct = asString(params.itemId ?? params.item_id ?? params.id);
if (direct) return direct;
const item = asRecord(params.item);
if (item) {
return asString(item.id ?? item.itemId ?? item.item_id);
}
return null;
}
function extractItem(params: Record<string, unknown>): Record<string, unknown> | null {
const item = asRecord(params.item);
return item ?? params;
}
function normalizeItemType(value: unknown): string | null {
const raw = asString(value);
if (!raw) return null;
return raw.toLowerCase().replace(/[\s_-]/g, '');
}
function extractCommand(value: unknown): string | null {
if (typeof value === 'string') return value;
if (Array.isArray(value)) {
const parts = value.filter((part): part is string => typeof part === 'string');
return parts.length > 0 ? parts.join(' ') : null;
}
return null;
}
function extractGeneratedImagePath(item: Record<string, unknown>): string | null {
return asString(
item.savedPath
?? item.saved_path
?? item.path
?? item.filePath
?? item.file_path
?? item.outputPath
?? item.output_path
);
}
function extractGeneratedImageMimeType(item: Record<string, unknown>): string | null {
return asString(item.mimeType ?? item.mime_type ?? item.mediaType ?? item.media_type);
}
function extractGeneratedImageFileName(item: Record<string, unknown>, savedPath: string): string {
const direct = asString(item.fileName ?? item.file_name ?? item.filename ?? item.name);
if (direct) return direct;
return savedPath.split(/[\\/]/).filter(Boolean).pop() ?? 'generated-image.png';
}
function extractChanges(value: unknown): Record<string, unknown> | null {
const record = asRecord(value);
if (record) return record;
if (Array.isArray(value)) {
const changes: Record<string, unknown> = {};
for (const entry of value) {
const entryRecord = asRecord(entry);
if (!entryRecord) continue;
const path = asString(entryRecord.path ?? entryRecord.file ?? entryRecord.filePath ?? entryRecord.file_path);
if (path) {
changes[path] = entryRecord;
}
}
return Object.keys(changes).length > 0 ? changes : null;
}
return null;
}
function extractTextFromContent(value: unknown): string | null {
if (typeof value === 'string' && value.length > 0) {
return value;
}
if (!Array.isArray(value)) {
return null;
}
const chunks: string[] = [];
for (const entry of value) {
const record = asRecord(entry);
if (!record) continue;
const text = asString(record.text ?? record.message ?? record.content);
if (text) {
chunks.push(text);
}
}
if (chunks.length === 0) {
return null;
}
return chunks.join('');
}
function extractItemText(item: Record<string, unknown>): string | null {
return asString(item.text ?? item.message) ?? extractTextFromContent(item.content);
}
function extractReasoningText(item: Record<string, unknown>): string | null {
const direct = extractItemText(item);
if (direct) {
return direct;
}
const summary = item.summary_text ?? item.summaryText;
if (Array.isArray(summary)) {
const chunks = summary.filter((part): part is string => typeof part === 'string' && part.length > 0);
if (chunks.length > 0) {
return chunks.join('\n');
}
}
return null;
}
function normalizePlanStatus(value: unknown): 'pending' | 'in_progress' | 'completed' {
const raw = typeof value === 'string' ? value.trim().toLowerCase().replace(/[\s-]/g, '_') : '';
if (raw === 'completed' || raw === 'complete' || raw === 'done') return 'completed';
if (raw === 'in_progress' || raw === 'inprogress' || raw === 'active' || raw === 'running') return 'in_progress';
return 'pending';
}
function extractPlanEntries(value: unknown): Array<{ step: string; status: 'pending' | 'in_progress' | 'completed' }> {
const record = asRecord(value);
const entries = Array.isArray(value)
? value
: Array.isArray(record?.plan)
? record.plan
: Array.isArray(record?.items)
? record.items
: Array.isArray(record?.steps)
? record.steps
: [];
const plan: Array<{ step: string; status: 'pending' | 'in_progress' | 'completed' }> = [];
for (const entry of entries) {
if (typeof entry === 'string') {
plan.push({ step: entry, status: 'pending' });
continue;
}
const item = asRecord(entry);
if (!item) continue;
const step = asString(item.step ?? item.content ?? item.text ?? item.title ?? item.description);
if (!step) continue;
plan.push({
step,
status: normalizePlanStatus(item.status ?? item.state)
});
}
return plan;
}
function extractPlanUpdate(params: Record<string, unknown>): ConvertedEvent[] {
const plan = extractPlanEntries(
params.plan ?? params.update ?? params.items ?? params.steps ?? params
);
return plan.length > 0 ? [{ type: 'plan_update', plan }] : [];
}
function extractEventScope(params: Record<string, unknown>): Record<string, unknown> {
const thread = asRecord(params.thread);
const turn = asRecord(params.turn);
const tokenUsage = asRecord(params.tokenUsage ?? params.token_usage ?? params.info);
const tokenUsageThread = asRecord(tokenUsage?.thread);
const tokenUsageTurn = asRecord(tokenUsage?.turn);
const item = asRecord(params.item);
const itemThread = asRecord(item?.thread);
const itemTurn = asRecord(item?.turn);
const threadId = asString(
params.threadId
?? params.thread_id
?? thread?.threadId
?? thread?.thread_id
?? thread?.id
?? tokenUsage?.threadId
?? tokenUsage?.thread_id
?? tokenUsageThread?.threadId
?? tokenUsageThread?.thread_id
?? tokenUsageThread?.id
?? item?.threadId
?? item?.thread_id
?? itemThread?.threadId
?? itemThread?.thread_id
?? itemThread?.id
);
const turnId = asString(
params.turnId
?? params.turn_id
?? turn?.turnId
?? turn?.turn_id
?? turn?.id
?? tokenUsage?.turnId
?? tokenUsage?.turn_id
?? tokenUsageTurn?.turnId
?? tokenUsageTurn?.turn_id
?? tokenUsageTurn?.id
?? item?.turnId
?? item?.turn_id
?? itemTurn?.turnId
?? itemTurn?.turn_id
?? itemTurn?.id
);
return {
...(threadId ? { thread_id: threadId } : {}),
...(turnId ? { turn_id: turnId } : {})
};
}
function addEventScope(events: ConvertedEvent[], scope: Record<string, unknown>): ConvertedEvent[] {
if (Object.keys(scope).length === 0) {
return events;
}
return events.map((event) => ({
...scope,
...event
}));
}
const MAX_UNHANDLED_LOG_STRING_LENGTH = 512;
const MAX_UNHANDLED_LOG_ARRAY_LENGTH = 20;
const MAX_UNHANDLED_LOG_DEPTH = 8;
function sanitizeUnhandledNotificationLogValue(value: unknown, depth: number = 0): unknown {
if (typeof value === 'string') {
if (value.length <= MAX_UNHANDLED_LOG_STRING_LENGTH) {
return value;
}
return `${value.slice(0, MAX_UNHANDLED_LOG_STRING_LENGTH)}... [truncated ${value.length - MAX_UNHANDLED_LOG_STRING_LENGTH} chars for logs]`;
}
if (Array.isArray(value)) {
const items = value
.slice(0, MAX_UNHANDLED_LOG_ARRAY_LENGTH)
.map((item) => sanitizeUnhandledNotificationLogValue(item, depth + 1));
if (value.length > MAX_UNHANDLED_LOG_ARRAY_LENGTH) {
items.push(`... [truncated ${value.length - MAX_UNHANDLED_LOG_ARRAY_LENGTH} array items for logs]`);
}
return items;
}
if (!value || typeof value !== 'object') {
return value;
}
if (depth >= MAX_UNHANDLED_LOG_DEPTH) {
return '[truncated nested object for logs]';
}
const result: Record<string, unknown> = {};
for (const [key, nestedValue] of Object.entries(value)) {
result[key] = sanitizeUnhandledNotificationLogValue(nestedValue, depth + 1);
}
return result;
}
function normalizeCodexAgentToolName(value: unknown): string | null {
const raw = asString(value);
if (!raw) return null;
const normalized = raw.trim().toLowerCase().replace(/[\s_-]/g, '');
if (normalized === 'spawnagent' || normalized === 'spawn') return 'spawn_agent';
if (normalized === 'sendinput') return 'send_input';
if (normalized === 'sendmessage') return 'send_message';
if (normalized === 'resumeagent' || normalized === 'resume') return 'resume_agent';
if (normalized === 'followuptask' || normalized === 'assigntask') return 'followup_task';
if (normalized === 'waitagent' || normalized === 'wait') return 'wait_agent';
if (normalized === 'closeagent' || normalized === 'close') return 'close_agent';
if (normalized === 'interruptagent' || normalized === 'interrupt') return 'interrupt_agent';
if (normalized === 'listagents') return 'list_agents';
return null;
}
function normalizeCollabAgentToolName(value: unknown): string | null {
const toolName = normalizeCodexAgentToolName(value);
return toolName === 'spawn_agent'
|| toolName === 'send_input'
|| toolName === 'resume_agent'
|| toolName === 'wait_agent'
|| toolName === 'close_agent'
? toolName
: null;
}
function parseRawToolInput(value: unknown): unknown {
if (typeof value !== 'string') return value ?? {};
try {
return JSON.parse(value) as unknown;
} catch {
return value;
}
}
const MULTI_AGENT_V1_NAMESPACE = 'multi_agent_v1';
function isRawMultiAgentV2Call(
toolName: string,
input: unknown,
namespace: unknown
): boolean {
// V1 already has richer collabAgentToolCall lifecycle items. V2 uses the
// configurable namespace ("collaboration" by default), so only the fixed
// V1 namespace can be excluded here.
if (asString(namespace) === MULTI_AGENT_V1_NAMESPACE) return false;
if (
toolName === 'send_message'
|| toolName === 'followup_task'
|| toolName === 'interrupt_agent'
|| toolName === 'list_agents'
) {
return true;
}
const inputRecord = asRecord(input);
if (toolName === 'spawn_agent') {
return Boolean(asString(inputRecord?.task_name ?? inputRecord?.taskName));
}
if (toolName === 'wait_agent') {
return !Array.isArray(inputRecord?.targets);
}
return false;
}
function extractStringArray(value: unknown): string[] {
return Array.isArray(value)
? value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0)
: [];
}
function extractCodexErrorInfo(
record: Record<string, unknown>,
errorRecord: Record<string, unknown> | null
): string | null {
return asString(
record.codexErrorInfo
?? record.codex_error_info
?? errorRecord?.codexErrorInfo
?? errorRecord?.codex_error_info
);
}
function buildCollabAgentInput(item: Record<string, unknown>, toolName: string): Record<string, unknown> {
const targets = extractStringArray(item.receiverThreadIds ?? item.receiver_thread_ids ?? item.targets);
const input: Record<string, unknown> = {};
const prompt = asString(item.prompt ?? item.message);
if (prompt) {
input.message = prompt;
}
const agentType = asString(item.agentType ?? item.agent_type);
if (agentType) {
input.agent_type = agentType;
}
const forkContext = asBoolean(item.forkContext ?? item.fork_context);
if (forkContext !== null) {
input.fork_context = forkContext;
}
const model = asString(item.model);
if (model) {
input.model = model;
}
const reasoningEffort = asString(item.reasoningEffort ?? item.reasoning_effort);
if (reasoningEffort) {
input.reasoning_effort = reasoningEffort;
}
const senderThreadId = asString(item.senderThreadId ?? item.sender_thread_id);
if (senderThreadId) {
input.sender_thread_id = senderThreadId;
}
if (targets.length > 0) {
input.targets = targets;
if (toolName === 'close_agent' || toolName === 'send_input' || toolName === 'resume_agent') {
input.target = targets[0];
}
}
return input;
}
function statusObjectFromAgentState(value: unknown): unknown {
const record = asRecord(value);
if (!record) return value;
const message = asString(record.message)
?? asString(record.output)
?? asString(record.result)
?? asString(record.finalMessage)
?? asString(record.final_message);
const status = asString(record.status ?? record.state);
const normalizedStatus = status?.trim().toLowerCase().replace(/[\s_-]/g, '');
const completed = normalizedStatus === 'completed'
|| normalizedStatus === 'complete'
|| normalizedStatus === 'done'
|| record.completed === true
|| record.done === true;
if (completed && message) return { completed: message };
if (completed) return { ...record, status: 'completed' };
if ((normalizedStatus === 'failed' || normalizedStatus === 'error') && message) return { failed: message };
if ((normalizedStatus === 'canceled' || normalizedStatus === 'cancelled') && message) return { canceled: message };
return value;
}
function buildCollabAgentOutput(item: Record<string, unknown>, toolName: string): Record<string, unknown> {
const targets = extractStringArray(item.receiverThreadIds ?? item.receiver_thread_ids ?? item.targets);
const agentsStates = asRecord(item.agentsStates ?? item.agents_states) ?? {};
const status = asString(item.status);
const error = asString(item.error ?? item.message);
const errorFields = error ? { error, message: error } : {};
if (toolName === 'spawn_agent') {
const agentId = targets[0] ?? null;
return {
...(agentId ? { agent_id: agentId, agentId } : {}),
...(status ? { status } : {}),
...errorFields,
agentsStates
};
}
if (toolName === 'wait_agent') {
const normalizedStatus: Record<string, unknown> = {};
for (const [agentId, agentStatus] of Object.entries(agentsStates)) {
normalizedStatus[agentId] = statusObjectFromAgentState(agentStatus);
}
return {
status: normalizedStatus,
...errorFields,
timed_out: status === 'timedOut' || status === 'timed_out'
};
}
if (toolName === 'close_agent') {
const firstStatus = targets[0] ? agentsStates[targets[0]] : Object.values(agentsStates)[0];
return {
previous_status: statusObjectFromAgentState(firstStatus),
...errorFields,
...(targets[0] ? { agent_id: targets[0] } : {})
};
}
return {
...(targets.length > 0 ? { targets } : {}),
...(status ? { status } : {}),
...errorFields,
agentsStates
};
}
export class AppServerEventConverter {
private readonly agentMessageBuffers = new Map<string, string>();
private readonly reasoningBuffers = new Map<string, string>();
private readonly commandOutputBuffers = new Map<string, string>();
private readonly commandMeta = new Map<string, Record<string, unknown>>();
private readonly fileChangeMeta = new Map<string, Record<string, unknown>>();
private readonly completedAgentMessageItems = new Set<string>();
private readonly completedReasoningItems = new Set<string>();
private readonly reasoningSectionBreakKeys = new Set<string>();
private readonly lastAgentMessageDeltaByItemId = new Map<string, string>();
private readonly lastReasoningDeltaByItemId = new Map<string, string>();
private readonly lastCommandOutputDeltaByItemId = new Map<string, string>();
private readonly rawAgentToolCallIds = new Set<string>();
private readonly rawAgentToolNames = new Map<string, string>();
private handleWrappedCodexEvent(paramsRecord: Record<string, unknown>): ConvertedEvent[] | null {
const msg = asRecord(paramsRecord.msg);
if (!msg) {
return [];
}
const msgType = asString(msg.type);
if (!msgType) {
return [];
}
const msgScope = extractEventScope(msg);
if (msgType === 'item_started' || msgType === 'item_completed') {
const itemMethod = msgType === 'item_started' ? 'item/started' : 'item/completed';
const item = asRecord(msg.item) ?? {};
const threadId = asString(msg.thread_id ?? msg.threadId ?? msgScope.thread_id);
const turnId = asString(msg.turn_id ?? msg.turnId ?? msgScope.turn_id);
const params: Record<string, unknown> = {
...msgScope,
item,
itemId: asString(msg.item_id ?? msg.itemId ?? item.id),
...(threadId ? { threadId } : {}),
...(turnId ? { turnId } : {})
};
return this.handleNotification(itemMethod, params);
}
if (
msgType === 'task_started' ||
msgType === 'task_complete' ||
msgType === 'turn_aborted' ||
msgType === 'task_failed'
) {
const turnId = asString(msg.turn_id ?? msg.turnId ?? msgScope.turn_id);
if ((msgType === 'task_complete' || msgType === 'turn_aborted' || msgType === 'task_failed') && !turnId) {
logger.debug('[AppServerEventConverter] Ignoring wrapped terminal event without turn_id', { msgType });
return [];
}
const event: ConvertedEvent = { ...msgScope, type: msgType };
if (turnId) {
event.turn_id = turnId;
}
const threadId = asString(msg.thread_id ?? msg.threadId ?? msgScope.thread_id);
if (threadId) {
event.thread_id = threadId;
}
if (msgType === 'task_failed') {
const error = asString(msg.error ?? msg.message ?? asRecord(msg.error)?.message);
if (error) {
event.error = error;
}
}
return [event];
}
if (msgType === 'agent_message_delta' || msgType === 'agent_message_content_delta') {
const itemId = asString(msg.item_id ?? msg.itemId ?? msg.id) ?? 'agent-message';
const delta = asString(msg.delta ?? msg.text ?? msg.message);
if (!delta) return [];
return this.handleNotification('item/agentMessage/delta', { itemId, delta, ...msgScope });
}
if (msgType === 'reasoning_content_delta') {
const itemId = asString(msg.item_id ?? msg.itemId ?? msg.id) ?? 'reasoning';
const delta = asString(msg.delta ?? msg.text ?? msg.message);
if (!delta) return [];
return this.handleNotification('item/reasoning/summaryTextDelta', { itemId, delta, ...msgScope });
}
if (msgType === 'agent_reasoning_section_break') {
const itemId = asString(msg.item_id ?? msg.itemId ?? msg.id) ?? 'reasoning';
const summaryIndex = asNumber(msg.summary_index ?? msg.summaryIndex);
return this.handleNotification('item/reasoning/summaryPartAdded', {
itemId,
...msgScope,
...(summaryIndex !== null ? { summaryIndex } : {})
});
}
if (msgType === 'agent_reasoning_delta' || msgType === 'agent_reasoning' || msgType === 'agent_message') {
return [];
}
if (msgType === 'exec_command_output_delta') {
const itemId = asString(msg.call_id ?? msg.callId ?? msg.item_id ?? msg.itemId ?? msg.id);
const delta = asString(msg.delta ?? msg.output ?? msg.stdout ?? msg.text);
if (!itemId || !delta) return [];
return this.handleNotification('item/commandExecution/outputDelta', { itemId, delta, ...msgScope });
}
if (msgType === 'error') {
const errorRecord = asRecord(msg.error);
const retryable = asBoolean(msg.will_retry ?? msg.willRetry ?? errorRecord?.will_retry ?? errorRecord?.willRetry);
const willRetry = retryable ?? false;
if (willRetry) {
return [];
}
const error = asString(msg.message ?? msg.reason ?? errorRecord?.message);
const codexErrorInfo = extractCodexErrorInfo(msg, errorRecord);
return error ? addEventScope([{
type: 'task_failed',
...(retryable !== null ? { retryable } : {}),
...(codexErrorInfo ? { codex_error_info: codexErrorInfo } : {}),
error
}], msgScope) : [];
}
if (msgType === 'plan_update') {
return addEventScope(extractPlanUpdate(msg), msgScope);
}
if (msgType === 'context_compacted') {
const threadId = asString(msg.thread_id ?? msg.threadId ?? msgScope.thread_id);
if (!threadId) {
return [];
}
const turnId = asString(msg.turn_id ?? msg.turnId ?? msgScope.turn_id);
return [
{
type: 'thread_compacted',
thread_id: threadId,
...(turnId ? { turn_id: turnId } : {})
},
...addEventScope([{ type: 'context_compacted' }], msgScope)
];
}
if (
msgType === 'mcp_startup_update' ||
msgType === 'mcp_startup_complete' ||
msgType === 'skills_update_available' ||
msgType === 'stream_error' ||
msgType === 'warning' ||
msgType === 'terminal_interaction' ||
msgType === 'user_message'
) {
return [];
}
return addEventScope([msg as ConvertedEvent], msgScope);
}
handleNotification(method: string, params: unknown): ConvertedEvent[] {
const events: ConvertedEvent[] = [];
const paramsRecord = asRecord(params) ?? {};
const eventScope = extractEventScope(paramsRecord);
const scoped = (event: ConvertedEvent): ConvertedEvent => ({
...eventScope,
...event
});
if (method.startsWith('codex/event/')) {
return this.handleWrappedCodexEvent(paramsRecord) ?? events;
}
if (method === 'turn/plan/updated') {
return addEventScope(extractPlanUpdate(paramsRecord), eventScope);
}
if (method === 'account/rateLimits/updated') {
return events;
}
if (method === 'thread/compacted') {
const threadId = asString(paramsRecord.threadId ?? paramsRecord.thread_id ?? eventScope.thread_id);
if (!threadId) {
return events;
}
const turnId = asString(paramsRecord.turnId ?? paramsRecord.turn_id ?? eventScope.turn_id);
events.push({
type: 'thread_compacted',
thread_id: threadId,
...(turnId ? { turn_id: turnId } : {})
});
events.push(scoped({ type: 'context_compacted' }));
return events;
}
if (method === 'thread/goal/updated') {
const goal = asRecord(paramsRecord.goal);
const threadId = asString(paramsRecord.threadId ?? paramsRecord.thread_id ?? goal?.threadId ?? goal?.thread_id);
if (!threadId || !goal) {
return events;
}
const turnId = asString(paramsRecord.turnId ?? paramsRecord.turn_id);
events.push({
type: 'thread_goal_updated',
thread_id: threadId,
...(turnId ? { turn_id: turnId } : {}),
goal
});
return events;
}
if (method === 'thread/goal/cleared') {
const threadId = asString(paramsRecord.threadId ?? paramsRecord.thread_id ?? eventScope.thread_id);
if (!threadId) {
return events;
}
events.push({
type: 'thread_goal_cleared',
thread_id: threadId
});
return events;
}
if (method === 'thread/started' || method === 'thread/resumed') {
const thread = asRecord(paramsRecord.thread) ?? paramsRecord;
const threadId = asString(thread.threadId ?? thread.thread_id ?? thread.id);
if (threadId) {
events.push({ type: 'thread_started', thread_id: threadId });
}
return events;
}
if (method === 'thread/status/changed') {
const thread = asRecord(paramsRecord.thread) ?? paramsRecord;
const threadId = asString(thread.threadId ?? thread.thread_id ?? thread.id);
const status = asRecord(paramsRecord.status ?? thread.status);
const statusType = asString(status?.type ?? paramsRecord.statusType ?? paramsRecord.status_type);
if (statusType === 'systemError') {
const error = asString(status?.message ?? status?.error ?? paramsRecord.message ?? paramsRecord.error)
?? 'Codex thread entered systemError';
events.push(scoped({
type: 'task_failed',
...(threadId ? { thread_id: threadId } : {}),
terminal_source: 'thread_status',
error
}));
}
return events;
}
if (method === 'turn/started') {
const turn = asRecord(paramsRecord.turn) ?? paramsRecord;
const turnId = asString(turn.turnId ?? turn.turn_id ?? turn.id);
events.push(scoped({ type: 'task_started', ...(turnId ? { turn_id: turnId } : {}) }));
return events;
}
if (method === 'turn/completed') {
const turn = asRecord(paramsRecord.turn) ?? paramsRecord;
const statusRaw = asString(paramsRecord.status ?? turn.status);
const status = statusRaw?.toLowerCase();
const turnId = asString(turn.turnId ?? turn.turn_id ?? turn.id);
const turnError = asRecord(paramsRecord.error ?? turn.error);
const errorMessage = asString(paramsRecord.error ?? paramsRecord.message ?? paramsRecord.reason)
?? asString(turnError?.message);
if (status === 'interrupted' || status === 'cancelled' || status === 'canceled') {
events.push(scoped({ type: 'turn_aborted', ...(turnId ? { turn_id: turnId } : {}) }));
return events;
}
if (status === 'failed' || status === 'error') {
const codexErrorInfo = extractCodexErrorInfo(paramsRecord, turnError);
events.push(scoped({
type: 'task_failed',
...(turnId ? { turn_id: turnId } : {}),
terminal_source: 'turn_completed',
...(codexErrorInfo ? { codex_error_info: codexErrorInfo } : {}),
...(errorMessage ? { error: errorMessage } : {})
}));
return events;
}
events.push(scoped({ type: 'task_complete', ...(turnId ? { turn_id: turnId } : {}) }));
return events;
}
if (method === 'turn/diff/updated') {
const diff = asString(paramsRecord.diff ?? paramsRecord.unified_diff ?? paramsRecord.unifiedDiff);
if (diff) {
events.push(scoped({ type: 'turn_diff', unified_diff: diff }));
}
return events;
}
if (method === 'thread/tokenUsage/updated') {
const info = asRecord(paramsRecord.tokenUsage ?? paramsRecord.token_usage ?? paramsRecord) ?? {};
events.push(scoped({ type: 'token_count', info }));
return events;
}
if (method === 'model/safetyBuffering/updated') {
const model = asString(paramsRecord.model);
const showBufferingUi = asBoolean(paramsRecord.showBufferingUi ?? paramsRecord.show_buffering_ui);
if (showBufferingUi === null || (showBufferingUi && !model)) {
return events;
}
events.push(scoped({
type: 'model_safety_buffering',
...(model ? { model } : {}),
use_cases: extractStringArray(paramsRecord.useCases ?? paramsRecord.use_cases),
reasons: extractStringArray(paramsRecord.reasons),
show_buffering_ui: showBufferingUi,
faster_model: asString(paramsRecord.fasterModel ?? paramsRecord.faster_model)
}));
return events;
}
if (method === 'model/rerouted') {
const fromModel = asString(paramsRecord.fromModel ?? paramsRecord.from_model);
const toModel = asString(paramsRecord.toModel ?? paramsRecord.to_model);
const reason = asString(paramsRecord.reason);
if (fromModel && toModel && reason) {
events.push(scoped({
type: 'model_rerouted',
from_model: fromModel,
to_model: toModel,
reason
}));
}
return events;
}
if (method === 'model/verification') {
events.push(scoped({
type: 'model_verification',
verifications: extractStringArray(paramsRecord.verifications)
}));
return events;
}
if (method === 'error') {
const errorRecord = asRecord(paramsRecord.error);
const retryable = asBoolean(
paramsRecord.will_retry
?? paramsRecord.willRetry
?? errorRecord?.will_retry
?? errorRecord?.willRetry
);
const willRetry = retryable ?? false;
if (willRetry) return events;
const message = asString(paramsRecord.message) ?? asString(errorRecord?.message);
if (message) {
const codexErrorInfo = extractCodexErrorInfo(paramsRecord, errorRecord);
events.push(scoped({
type: 'task_failed',
terminal_source: 'error',
...(retryable !== null ? { retryable } : {}),
...(codexErrorInfo ? { codex_error_info: codexErrorInfo } : {}),
error: message
}));
}
return events;
}
if (method === 'rawResponseItem/completed') {
const item = asRecord(paramsRecord.item);
if (!item) return events;
const itemType = normalizeItemType(item.type);
const callId = asString(item.call_id ?? item.callId);
if (!itemType || !callId) return events;
if (itemType === 'functioncall') {
const toolName = normalizeCodexAgentToolName(item.name);
const input = parseRawToolInput(item.arguments);
if (
!toolName
|| !isRawMultiAgentV2Call(toolName, input, item.namespace)
|| this.rawAgentToolCallIds.has(callId)
) return events;
this.rawAgentToolCallIds.add(callId);
this.rawAgentToolNames.set(callId, toolName);
events.push(scoped({
type: 'codex_tool_call_begin',
call_id: callId,
name: toolName,
input
}));
return events;
}
if (itemType === 'functioncalloutput') {
const toolName = this.rawAgentToolNames.get(callId);
if (!toolName) return events;
this.rawAgentToolNames.delete(callId);
events.push(scoped({
type: 'codex_tool_call_end',
call_id: callId,
name: toolName,
output: item.output,
is_error: false
}));
}
return events;
}
if (method === 'item/agentMessage/delta') {
const itemId = extractItemId(paramsRecord);
const delta = asString(paramsRecord.delta ?? paramsRecord.text ?? paramsRecord.message);
if (itemId && delta) {
const lastDelta = this.lastAgentMessageDeltaByItemId.get(itemId);
if (lastDelta === delta) {
return events;
}
this.lastAgentMessageDeltaByItemId.set(itemId, delta);
const prev = this.agentMessageBuffers.get(itemId) ?? '';
this.agentMessageBuffers.set(itemId, prev + delta);
events.push(scoped({ type: 'agent_message_delta' }));
}
return events;
}
if (method === 'item/reasoning/textDelta' || method === 'item/reasoning/summaryTextDelta') {
const itemId = extractItemId(paramsRecord) ?? 'reasoning';
const delta = asString(paramsRecord.delta ?? paramsRecord.text ?? paramsRecord.message);
if (delta) {
const lastDelta = this.lastReasoningDeltaByItemId.get(itemId);
if (lastDelta === delta) {
return events;
}
this.lastReasoningDeltaByItemId.set(itemId, delta);
const prev = this.reasoningBuffers.get(itemId) ?? '';
this.reasoningBuffers.set(itemId, prev + delta);
events.push(scoped({ type: 'agent_reasoning_delta', delta }));
}
return events;
}
if (method === 'item/reasoning/summaryPartAdded') {
const itemId = extractItemId(paramsRecord) ?? 'reasoning';
const summaryIndex = asNumber(paramsRecord.summaryIndex ?? paramsRecord.summary_index);
if (summaryIndex !== null) {
const key = `${itemId}:${summaryIndex}`;
if (this.reasoningSectionBreakKeys.has(key)) {
return events;
}
this.reasoningSectionBreakKeys.add(key);
}
events.push(scoped({ type: 'agent_reasoning_section_break' }));
return events;
}
if (method === 'item/commandExecution/outputDelta') {
const itemId = extractItemId(paramsRecord);
const delta = asString(paramsRecord.delta ?? paramsRecord.text ?? paramsRecord.output ?? paramsRecord.stdout);
if (itemId && delta) {
const lastDelta = this.lastCommandOutputDeltaByItemId.get(itemId);
if (lastDelta === delta) {
return events;
}
this.lastCommandOutputDeltaByItemId.set(itemId, delta);
const prev = this.commandOutputBuffers.get(itemId) ?? '';
this.commandOutputBuffers.set(itemId, prev + delta);
}
return events;
}
if (method === 'item/started' || method === 'item/completed') {
const item = extractItem(paramsRecord);
if (!item) return events;
const itemType = normalizeItemType(item.type ?? item.itemType ?? item.kind);
const itemId = extractItemId(paramsRecord) ?? asString(item.id ?? item.itemId ?? item.item_id);
if (!itemType || !itemId) {
return events;
}
if (itemType === 'contextcompaction') {
if (method === 'item/completed') {
const threadId = asString(eventScope.thread_id);
const turnId = asString(eventScope.turn_id);
if (threadId) {
events.push({
type: 'thread_compacted',
thread_id: threadId,
...(turnId ? { turn_id: turnId } : {}),
await_turn_completion: true
});
events.push(scoped({ type: 'context_compacted' }));
}
}
return events;
}
if (itemType === 'agentmessage') {
if (method === 'item/completed') {
if (this.completedAgentMessageItems.has(itemId)) {
return events;
}
const text = extractItemText(item) ?? this.agentMessageBuffers.get(itemId);
if (text) {
events.push(scoped({ type: 'agent_message', message: text }));
this.completedAgentMessageItems.add(itemId);
this.agentMessageBuffers.delete(itemId);
}
this.lastAgentMessageDeltaByItemId.delete(itemId);
}
return events;
}
if (itemType === 'reasoning') {
if (method === 'item/completed') {
if (this.completedReasoningItems.has(itemId)) {
return events;
}
const text = extractReasoningText(item) ?? this.reasoningBuffers.get(itemId);
if (text) {
events.push(scoped({ type: 'agent_reasoning', text }));
this.completedReasoningItems.add(itemId);
this.reasoningBuffers.delete(itemId);
}
this.lastReasoningDeltaByItemId.delete(itemId);
}
return events;
}
if (itemType === 'commandexecution') {
if (method === 'item/started') {
const command = extractCommand(item.command ?? item.cmd ?? item.args);
const cwd = asString(item.cwd ?? item.workingDirectory ?? item.working_directory);
const autoApproved = asBoolean(item.autoApproved ?? item.auto_approved);
const commandActions = Array.isArray(item.commandActions)
? item.commandActions
: Array.isArray(item.command_actions)
? item.command_actions
: null;
const source = asString(item.source);
const meta: Record<string, unknown> = {};
if (command) meta.command = command;
if (cwd) meta.cwd = cwd;
if (autoApproved !== null) meta.auto_approved = autoApproved;
if (commandActions) meta.command_actions = commandActions;
if (source) meta.command_source = source;
this.commandMeta.set(itemId, meta);
events.push(scoped({
type: 'exec_command_begin',
call_id: itemId,
...meta
}));
}
if (method === 'item/completed') {
const meta = this.commandMeta.get(itemId) ?? {};
const output = asString(item.aggregatedOutput ?? item.aggregated_output ?? item.output ?? item.result ?? item.stdout)
?? this.commandOutputBuffers.get(itemId);
const stderr = asString(item.stderr);
const error = asString(item.error);
const exitCode = asNumber(item.exitCode ?? item.exit_code ?? item.exitcode);
const durationMs = asNumber(item.durationMs ?? item.duration_ms);
const status = asString(item.status);
events.push(scoped({
type: 'exec_command_end',
call_id: itemId,
...meta,
...(output ? { output } : {}),
...(stderr ? { stderr } : {}),
...(error ? { error } : {}),
...(exitCode !== null ? { exit_code: exitCode } : {}),
...(durationMs !== null ? { duration_ms: durationMs } : {}),
...(status ? { status } : {})
}));
this.commandMeta.delete(itemId);
this.commandOutputBuffers.delete(itemId);
this.lastCommandOutputDeltaByItemId.delete(itemId);
}
return events;
}
if (itemType === 'mcptoolcall') {
const server = asString(item.server ?? item.serverName ?? item.server_name);
const tool = asString(item.tool ?? item.toolName ?? item.tool_name ?? item.name);
const input = item.arguments ?? item.input ?? {};
if (method === 'item/started') {
events.push(scoped({
type: 'mcp_tool_call_begin',
call_id: itemId,
server,
tool,
invocation: {
server,
tool,
arguments: input
}
}));
}
if (method === 'item/completed') {
const error = item.error;
events.push(scoped({
type: 'mcp_tool_call_end',
call_id: itemId,
server,
tool,
result: error ? { Err: error } : item.result
}));
}
return events;
}
if (itemType === 'imagegeneration') {
if (method === 'item/completed') {
const savedPath = extractGeneratedImagePath(item);
if (!savedPath) {
logger.debug('[AppServerEventConverter] imageGeneration missing savedPath', sanitizeUnhandledNotificationLogValue({ item }));
return events;
}
events.push(scoped({
type: 'generated_image',
image_id: itemId,
saved_path: savedPath,
file_name: extractGeneratedImageFileName(item, savedPath),
...(extractGeneratedImageMimeType(item) ? { mime_type: extractGeneratedImageMimeType(item) } : {})
}));
}
return events;
}
if (itemType === 'collabagenttoolcall') {
if (this.rawAgentToolCallIds.has(itemId)) return events;
const toolName = normalizeCollabAgentToolName(item.tool ?? item.name);
if (!toolName) return events;
if (method === 'item/started') {
events.push(scoped({
type: 'codex_tool_call_begin',
call_id: itemId,
name: toolName,
input: buildCollabAgentInput(item, toolName)
}));
}
if (method === 'item/completed') {
const status = asString(item.status);
events.push(scoped({
type: 'codex_tool_call_end',
call_id: itemId,
name: toolName,
output: buildCollabAgentOutput(item, toolName),
is_error: status === 'failed' || status === 'error'
}));
}
return events;
}
if (itemType === 'filechange') {
if (method === 'item/started') {
const changes = extractChanges(item.changes ?? item.change ?? item.diff);
const autoApproved = asBoolean(item.autoApproved ?? item.auto_approved);
const meta: Record<string, unknown> = {};
if (changes) meta.changes = changes;
if (autoApproved !== null) meta.auto_approved = autoApproved;
this.fileChangeMeta.set(itemId, meta);
events.push(scoped({
type: 'patch_apply_begin',
call_id: itemId,
...meta
}));
}
if (method === 'item/completed') {
const meta = this.fileChangeMeta.get(itemId) ?? {};
const stdout = asString(item.stdout ?? item.output);
const stderr = asString(item.stderr);
const success = asBoolean(item.success ?? item.ok ?? item.applied ?? item.status === 'completed');
events.push(scoped({
type: 'patch_apply_end',
call_id: itemId,
...meta,
...(stdout ? { stdout } : {}),
...(stderr ? { stderr } : {}),
success: success ?? false
}));
this.fileChangeMeta.delete(itemId);
}
return events;
}
}
logger.debug('[AppServerEventConverter] Unhandled notification', sanitizeUnhandledNotificationLogValue({ method, params }));
return events;
}
reset(): void {
this.agentMessageBuffers.clear();
this.reasoningBuffers.clear();
this.commandOutputBuffers.clear();
this.commandMeta.clear();
this.fileChangeMeta.clear();
this.completedAgentMessageItems.clear();
this.completedReasoningItems.clear();
this.reasoningSectionBreakKeys.clear();
this.lastAgentMessageDeltaByItemId.clear();
this.lastReasoningDeltaByItemId.clear();
this.lastCommandOutputDeltaByItemId.clear();
this.rawAgentToolCallIds.clear();
this.rawAgentToolNames.clear();
}
}