import type { AgentJournalMessageItem, AgentJournalSubmission } from './agent-session-journal-types' import { agentSessionRefusalOperationState } from './agent-session-refusal-retry' import type { AgentSessionWireRefusalCode } from './agent-session-wire' import { structuredAgentSessionPayloadFingerprint } from './structured-agent-session-mutation' import { DISPATCH_REJECTED_CANCELLED } from './structured-agent-session-dispatch-rejection' export type StructuredAgentSessionOutboxState = 'queued' | 'dispatching' | 'unconfirmed' export type StructuredAgentSessionOutboxEntry = { clientMessageId: string sessionId: string body: AgentJournalMessageItem previewUris: string[] state: StructuredAgentSessionOutboxState queuedAt: number lastAttemptAt: number | null retryAfterUnknownSubmittedAt: number | null source?: 'launch' } export type StructuredAgentSessionAttachment = { path: string previewUri: string } export function structuredAgentSessionSendBody( text: string, attachments: readonly StructuredAgentSessionAttachment[] ): AgentJournalMessageItem { return { kind: 'message', role: 'user', blocks: [ ...(text.trim().length > 0 ? [{ type: 'text' as const, text: text.trimEnd() }] : []), ...attachments.map((attachment) => ({ type: 'image-ref' as const, path: attachment.path })) ] } } export function createStructuredAgentSessionOutboxEntry(args: { clientMessageId: string sessionId: string text: string attachments: readonly StructuredAgentSessionAttachment[] queuedAt: number }): StructuredAgentSessionOutboxEntry { return { clientMessageId: args.clientMessageId, sessionId: args.sessionId, body: structuredAgentSessionSendBody(args.text, args.attachments), previewUris: args.attachments.map((attachment) => attachment.previewUri), state: 'queued', queuedAt: args.queuedAt, lastAttemptAt: null, retryAfterUnknownSubmittedAt: null } } export function updateStructuredAgentSessionOutboxEntry( entries: readonly StructuredAgentSessionOutboxEntry[], id: string, update: (entry: StructuredAgentSessionOutboxEntry) => StructuredAgentSessionOutboxEntry | null ): StructuredAgentSessionOutboxEntry[] { return entries.flatMap((entry) => { if (entry.clientMessageId !== id) { return [entry] } const next = update(entry) return next ? [next] : [] }) } export function requeueStructuredAgentSessionSendRefusal( entry: StructuredAgentSessionOutboxEntry, code: AgentSessionWireRefusalCode, createOperationId: () => string, retainOperationId = false ): StructuredAgentSessionOutboxEntry { const refusalState = agentSessionRefusalOperationState('agentSession.send', code) if ( refusalState !== 'settled-rejected' || retainOperationId || entry.state === 'unconfirmed' || entry.retryAfterUnknownSubmittedAt !== null ) { return { ...entry, state: 'queued' } } return { ...entry, clientMessageId: createOperationId(), state: 'queued', lastAttemptAt: null, retryAfterUnknownSubmittedAt: null } } export function reconcileStructuredAgentSessionOutbox( entries: readonly StructuredAgentSessionOutboxEntry[], submissions: readonly AgentJournalSubmission[] ): StructuredAgentSessionOutboxEntry[] { const settled = new Map(submissions.map((entry) => [entry.clientMessageId, entry])) return entries.flatMap((entry) => { const submission = settled.get(entry.clientMessageId) if (submission?.dispatchState === 'accepted') { return [] } if ( submission?.dispatchState === 'rejected' && submission.reason === DISPATCH_REJECTED_CANCELLED ) { return [] } if (submission?.dispatchState === 'pending') { return entry.state === 'dispatching' ? [entry] : [{ ...entry, state: 'dispatching' as const }] } if ( submission?.dispatchState === 'unknown' && entry.retryAfterUnknownSubmittedAt !== -1 && entry.retryAfterUnknownSubmittedAt !== submission.submittedAt ) { return [{ ...entry, state: 'unconfirmed' as const }] } return [entry] }) } export function parseStructuredAgentSessionOutboxEntry( value: unknown, sessionId: string ): StructuredAgentSessionOutboxEntry | null { if (typeof value !== 'object' || value === null) { return null } const entry = value as Partial const body = entry.body if ( entry.sessionId !== sessionId || typeof entry.clientMessageId !== 'string' || typeof entry.queuedAt !== 'number' || !body || body.kind !== 'message' || body.role !== 'user' || !Array.isArray(body.blocks) || !Array.isArray(entry.previewUris) || !entry.previewUris.every((uri) => typeof uri === 'string') || !['queued', 'dispatching', 'unconfirmed'].includes(entry.state ?? '') ) { return null } return { clientMessageId: entry.clientMessageId, sessionId, body, previewUris: entry.previewUris, state: entry.state as StructuredAgentSessionOutboxState, queuedAt: entry.queuedAt, lastAttemptAt: typeof entry.lastAttemptAt === 'number' ? entry.lastAttemptAt : null, retryAfterUnknownSubmittedAt: typeof entry.retryAfterUnknownSubmittedAt === 'number' ? entry.retryAfterUnknownSubmittedAt : null, ...(entry.source === 'launch' ? { source: 'launch' as const } : {}) } } export function structuredAgentSessionSendRequest( entry: StructuredAgentSessionOutboxEntry, expectedRuntimeFence: number ): Record { const fields = { body: entry.body } return { envelope: { sessionId: entry.sessionId, clientOperationId: entry.clientMessageId, expectedRuntimeFence, payloadFingerprint: structuredAgentSessionPayloadFingerprint({ method: 'agentSession.send', sessionId: entry.sessionId, fields }) }, ...fields } } export type StructuredAgentSessionSendFailure = 'delivery-unknown' | 'failed' export function classifyStructuredAgentSessionSendFailure( error: unknown, isDeliveryUnknown: (error: unknown) => boolean ): StructuredAgentSessionSendFailure { return isDeliveryUnknown(error) ? 'delivery-unknown' : 'failed' }