import { isTransportConnectionErrorMessage } from "@t3tools/client-runtime/errors"; import type { EnvironmentShellStatus } from "@t3tools/client-runtime/state/shell"; import { CommandId, EnvironmentId, IsoDateTime, MessageId, ModelSelection, ProviderInteractionMode, RuntimeMode, ThreadId, type ModelSelection as ModelSelectionType, type ProviderInteractionMode as ProviderInteractionModeType, type RuntimeMode as RuntimeModeType, } from "@t3tools/contracts"; import * as Schema from "effect/Schema"; import { DraftComposerImageAttachmentSchema } from "../lib/composer-image-schema"; import type { DraftComposerImageAttachment } from "../lib/composerImages"; import { scopedThreadKey } from "../lib/scopedEntities"; const THREAD_OUTBOX_SCHEMA_VERSION = 2; const THREAD_OUTBOX_MAX_RETRY_DELAY_MS = 16_000; export const QueuedThreadMessageSchema = Schema.Struct({ schemaVersion: Schema.Literals([1, THREAD_OUTBOX_SCHEMA_VERSION]), environmentId: EnvironmentId, threadId: ThreadId, messageId: MessageId, commandId: CommandId, text: Schema.String, attachments: Schema.Array(DraftComposerImageAttachmentSchema), modelSelection: Schema.optional(ModelSelection), runtimeMode: Schema.optional(RuntimeMode), interactionMode: Schema.optional(ProviderInteractionMode), createdAt: IsoDateTime, }); const decodeStoredQueuedThreadMessage = Schema.decodeUnknownSync(QueuedThreadMessageSchema); const encodeStoredQueuedThreadMessage = Schema.encodeUnknownSync(QueuedThreadMessageSchema); export interface QueuedThreadMessage { readonly environmentId: EnvironmentId; readonly threadId: ThreadId; readonly messageId: MessageId; readonly commandId: CommandId; readonly text: string; readonly attachments: ReadonlyArray; readonly modelSelection?: ModelSelectionType; readonly runtimeMode?: RuntimeModeType; readonly interactionMode?: ProviderInteractionModeType; readonly createdAt: string; } export interface ThreadSettingsSnapshot { readonly modelSelection: ModelSelectionType; readonly runtimeMode: RuntimeModeType; readonly interactionMode: ProviderInteractionModeType; } export function resolveQueuedThreadSettings( message: QueuedThreadMessage, thread: ThreadSettingsSnapshot, ): ThreadSettingsSnapshot { return { modelSelection: message.modelSelection ?? thread.modelSelection, runtimeMode: message.runtimeMode ?? thread.runtimeMode, interactionMode: message.interactionMode ?? thread.interactionMode, }; } export function modelSelectionsEqual(left: ModelSelectionType, right: ModelSelectionType): boolean { return ( left.instanceId === right.instanceId && left.model === right.model && JSON.stringify(left.options ?? null) === JSON.stringify(right.options ?? null) ); } export function encodeQueuedThreadMessage(message: QueuedThreadMessage): unknown { return encodeStoredQueuedThreadMessage({ schemaVersion: THREAD_OUTBOX_SCHEMA_VERSION, ...message, }); } export function decodeQueuedThreadMessage(value: unknown): QueuedThreadMessage { const { schemaVersion: _, ...message } = decodeStoredQueuedThreadMessage(value); return message; } export function groupQueuedThreadMessages( messages: ReadonlyArray, ): Record> { const deduplicated = new Map(); for (const message of messages) { deduplicated.set(message.messageId, message); } const grouped: Record> = {}; for (const message of deduplicated.values()) { const threadKey = scopedThreadKey(message.environmentId, message.threadId); (grouped[threadKey] ??= []).push(message); } for (const queue of Object.values(grouped)) { queue.sort((left, right) => left.createdAt.localeCompare(right.createdAt)); } return grouped; } export function flattenQueuedThreadMessages( queues: Record>, ): ReadonlyArray { return Object.values(queues).flat(); } export function threadOutboxRetryDelayMs(attempt: number): number { return Math.min(1_000 * 2 ** Math.max(0, attempt - 1), THREAD_OUTBOX_MAX_RETRY_DELAY_MS); } export type ThreadOutboxDeliveryAction = "wait" | "remove" | "send"; export function resolveThreadOutboxDeliveryAction(input: { readonly threadExists: boolean; readonly shellStatus: EnvironmentShellStatus; readonly environmentConnected: boolean; readonly threadBusy: boolean; }): ThreadOutboxDeliveryAction { if (!input.threadExists) { return input.shellStatus === "live" ? "remove" : "wait"; } return input.environmentConnected && !input.threadBusy ? "send" : "wait"; } function errorMessage(error: unknown): string | null { if (error instanceof Error) { return error.message; } if (typeof error === "object" && error !== null && "message" in error) { return typeof error.message === "string" ? error.message : null; } return typeof error === "string" ? error : null; } export function shouldRetryThreadOutboxDelivery(error: unknown): boolean { if ( typeof error === "object" && error !== null && "_tag" in error && error._tag === "ConnectionTransientError" ) { return true; } return isTransportConnectionErrorMessage(errorMessage(error)); } export type ThreadOutboxCommandStage = "settings-sync" | "start-turn"; export type ThreadOutboxFailureAction = "retry" | "discard"; export function resolveThreadOutboxFailureAction(input: { readonly stage: ThreadOutboxCommandStage; readonly error: unknown; readonly interrupted: boolean; }): ThreadOutboxFailureAction { if ( input.stage === "settings-sync" || input.interrupted || shouldRetryThreadOutboxDelivery(input.error) ) { return "retry"; } return "discard"; }