260 lines
10 KiB
TypeScript
260 lines
10 KiB
TypeScript
import type { SendMessageResult } from '@shared/types';
|
|
|
|
export interface OpenCodeRuntimeDeliveryDebugDetails {
|
|
messageId: string;
|
|
statusMessageId?: string;
|
|
providerId: string;
|
|
delivered: boolean | null;
|
|
responsePending: boolean | null;
|
|
responseState: string | null;
|
|
ledgerStatus: string | null;
|
|
visibleReplyMessageId?: string | null;
|
|
visibleReplyCorrelation?: string | null;
|
|
queuedBehindMessageId?: string | null;
|
|
acceptanceUnknown: boolean | null;
|
|
reason: string | null;
|
|
diagnostics: string[];
|
|
userVisibleState?: string | null;
|
|
userVisibleReasonCode?: string | null;
|
|
userVisibleMessage?: string | null;
|
|
userVisibleNextReviewAt?: string | null;
|
|
}
|
|
|
|
interface OpenCodeRuntimeDeliveryDiagnostics {
|
|
warning: string | null;
|
|
debugDetails: OpenCodeRuntimeDeliveryDebugDetails | null;
|
|
}
|
|
|
|
const PENDING_WARNING =
|
|
'OpenCode delivery is still being checked. Message was saved and will be observed before retry if needed.';
|
|
const PROOF_WARNING =
|
|
'OpenCode reply could not be verified. Message was saved to inbox, but no visible reply or task progress proof was found.';
|
|
const FAILED_WARNING =
|
|
'OpenCode runtime delivery failed. Message was saved to inbox, but live delivery did not complete.';
|
|
const ATTACHMENT_FAILED_WARNING =
|
|
'OpenCode attachment was not sent. Message was saved to inbox, but live delivery cannot include this attachment.';
|
|
|
|
function isOpenCodeAttachmentDeliveryFailureReason(reason: string | null | undefined): boolean {
|
|
const normalized = reason?.trim().toLowerCase();
|
|
return (
|
|
normalized === 'opencode_attachment_delivery_prepare_failed' ||
|
|
normalized?.startsWith('attachment_') === true ||
|
|
normalized?.startsWith('opencode_attachment_delivery_prepare_failed:') === true
|
|
);
|
|
}
|
|
|
|
function formatOpenCodeRuntimeDeliveryFailureReason(reason: string | null | undefined): string {
|
|
const normalized = reason?.trim();
|
|
if (!normalized) {
|
|
return '';
|
|
}
|
|
const normalizedLower = normalized.toLowerCase();
|
|
if (normalizedLower === 'empty_assistant_turn') {
|
|
return 'OpenCode returned an empty assistant turn.';
|
|
}
|
|
if (normalizedLower === 'prompt_delivered_no_assistant_message') {
|
|
return 'OpenCode accepted the prompt, but no assistant turn was recorded.';
|
|
}
|
|
if (
|
|
normalizedLower === 'visible_reply_still_required' ||
|
|
normalizedLower === 'visible_reply_ack_only_still_requires_answer' ||
|
|
normalizedLower === 'plain_text_ack_only_still_requires_answer'
|
|
) {
|
|
return 'OpenCode responded, but did not create a visible message_send reply.';
|
|
}
|
|
if (
|
|
normalizedLower === 'visible_reply_destination_not_found_yet' ||
|
|
normalizedLower === 'visible_reply_missing_relayofmessageid'
|
|
) {
|
|
return 'OpenCode created a reply without the required relayOfMessageId correlation.';
|
|
}
|
|
if (normalizedLower === 'visible_reply_missing_task_refs') {
|
|
return 'OpenCode created a reply without the required taskRefs metadata.';
|
|
}
|
|
if (normalizedLower === 'visible_reply_missing_task_refs_after_merge') {
|
|
return 'OpenCode created a reply without the required taskRefs metadata.';
|
|
}
|
|
if (normalizedLower === 'visible_reply_task_refs_merge_failed') {
|
|
return 'OpenCode created a reply without the required taskRefs metadata, and the app could not attach it automatically.';
|
|
}
|
|
if (normalizedLower === 'non_visible_tool_without_task_progress') {
|
|
return 'OpenCode used tools, but did not create a visible reply or task progress proof.';
|
|
}
|
|
if (normalizedLower === 'attachment_model_unsupported') {
|
|
return 'This OpenCode model is not verified for image attachments. Choose a vision-capable model or remove the image.';
|
|
}
|
|
if (normalizedLower === 'attachment_type_unsupported') {
|
|
return 'This OpenCode model cannot receive this attachment type. Remove the attachment or choose a supported image model.';
|
|
}
|
|
if (normalizedLower === 'attachment_too_large') {
|
|
return 'The attachment is too large for live OpenCode delivery. Reduce the image size or remove the attachment.';
|
|
}
|
|
if (
|
|
normalizedLower === 'attachment_artifact_missing' ||
|
|
normalizedLower === 'attachment_artifact_path_unsafe'
|
|
) {
|
|
return 'The attachment file is not available for live OpenCode delivery. Reattach the file and try again.';
|
|
}
|
|
if (normalizedLower === 'attachment_optimization_failed') {
|
|
return 'The attachment could not be optimized for live OpenCode delivery. Try a smaller image or remove the attachment.';
|
|
}
|
|
if (normalizedLower === 'attachment_provider_rejected') {
|
|
return 'The OpenCode provider rejected the attachment. Choose a different model or remove the attachment.';
|
|
}
|
|
if (normalizedLower === 'attachment_runtime_transport_failed') {
|
|
return 'OpenCode could not transport the attachment to the runtime. Try again or remove the attachment.';
|
|
}
|
|
if (normalizedLower.startsWith('opencode_attachment_delivery_prepare_failed:')) {
|
|
return normalized.slice('opencode_attachment_delivery_prepare_failed:'.length).trim();
|
|
}
|
|
return '';
|
|
}
|
|
|
|
export function buildOpenCodeRuntimeDeliveryDiagnostics(
|
|
result: SendMessageResult
|
|
): OpenCodeRuntimeDeliveryDiagnostics {
|
|
const runtimeDelivery = result.runtimeDelivery;
|
|
if (runtimeDelivery?.attempted !== true) {
|
|
return { warning: null, debugDetails: null };
|
|
}
|
|
|
|
const userVisibleState = runtimeDelivery.userVisibleImpact?.state;
|
|
const isFailed =
|
|
userVisibleState === 'error' || (!userVisibleState && runtimeDelivery.delivered === false);
|
|
const isWarning = userVisibleState === 'warning';
|
|
const isPending =
|
|
userVisibleState === 'checking' ||
|
|
(!userVisibleState && runtimeDelivery.responsePending === true);
|
|
if (!isFailed && !isPending) {
|
|
if (!isWarning) {
|
|
return { warning: null, debugDetails: null };
|
|
}
|
|
}
|
|
|
|
const userVisibleMessage = runtimeDelivery.userVisibleImpact?.message?.trim();
|
|
const candidateFailureReason =
|
|
userVisibleMessage ?? runtimeDelivery.reason ?? runtimeDelivery.diagnostics?.[0];
|
|
const mappedFailureReason =
|
|
isFailed || isWarning ? formatOpenCodeRuntimeDeliveryFailureReason(candidateFailureReason) : '';
|
|
const failureReason = mappedFailureReason || (isFailed || isWarning ? userVisibleMessage : '');
|
|
const isAttachmentFailure =
|
|
isFailed &&
|
|
(isOpenCodeAttachmentDeliveryFailureReason(runtimeDelivery.reason) ||
|
|
isOpenCodeAttachmentDeliveryFailureReason(runtimeDelivery.diagnostics?.[0]) ||
|
|
isOpenCodeAttachmentDeliveryFailureReason(candidateFailureReason));
|
|
const statusMessageId = runtimeDelivery.queuedBehindMessageId ?? result.messageId;
|
|
|
|
return {
|
|
warning:
|
|
isWarning && failureReason
|
|
? `${PROOF_WARNING} Reason: ${failureReason}`
|
|
: isWarning
|
|
? PROOF_WARNING
|
|
: isAttachmentFailure && failureReason
|
|
? `${ATTACHMENT_FAILED_WARNING} Reason: ${failureReason}`
|
|
: isAttachmentFailure
|
|
? ATTACHMENT_FAILED_WARNING
|
|
: isFailed && failureReason
|
|
? `${FAILED_WARNING} Reason: ${failureReason}`
|
|
: isFailed
|
|
? FAILED_WARNING
|
|
: PENDING_WARNING,
|
|
debugDetails: {
|
|
messageId: result.messageId,
|
|
statusMessageId,
|
|
providerId: runtimeDelivery.providerId,
|
|
delivered: typeof runtimeDelivery.delivered === 'boolean' ? runtimeDelivery.delivered : null,
|
|
responsePending:
|
|
typeof runtimeDelivery.responsePending === 'boolean'
|
|
? runtimeDelivery.responsePending
|
|
: null,
|
|
responseState: runtimeDelivery.responseState ?? null,
|
|
ledgerStatus: runtimeDelivery.ledgerStatus ?? null,
|
|
visibleReplyMessageId: runtimeDelivery.visibleReplyMessageId ?? null,
|
|
visibleReplyCorrelation: runtimeDelivery.visibleReplyCorrelation ?? null,
|
|
queuedBehindMessageId: runtimeDelivery.queuedBehindMessageId ?? null,
|
|
acceptanceUnknown:
|
|
typeof runtimeDelivery.acceptanceUnknown === 'boolean'
|
|
? runtimeDelivery.acceptanceUnknown
|
|
: null,
|
|
reason: runtimeDelivery.reason ?? null,
|
|
diagnostics: runtimeDelivery.diagnostics ?? [],
|
|
userVisibleState: runtimeDelivery.userVisibleImpact?.state ?? null,
|
|
userVisibleReasonCode: runtimeDelivery.userVisibleImpact?.reasonCode ?? null,
|
|
userVisibleMessage: runtimeDelivery.userVisibleImpact?.message ?? null,
|
|
userVisibleNextReviewAt: runtimeDelivery.userVisibleImpact?.nextReviewAt ?? null,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function isOpenCodeRuntimeDeliveryHardUxFailure(
|
|
runtimeDelivery: SendMessageResult['runtimeDelivery'] | null | undefined
|
|
): boolean {
|
|
if (runtimeDelivery?.attempted !== true) {
|
|
return false;
|
|
}
|
|
const userVisibleState = runtimeDelivery.userVisibleImpact?.state;
|
|
if (userVisibleState) {
|
|
return userVisibleState === 'error';
|
|
}
|
|
return runtimeDelivery.delivered === false;
|
|
}
|
|
|
|
export function isOpenCodeRuntimeDeliveryHardUxFailureFromDebugDetails(
|
|
details: OpenCodeRuntimeDeliveryDebugDetails | null | undefined
|
|
): boolean {
|
|
if (!details) {
|
|
return false;
|
|
}
|
|
if (details.userVisibleState) {
|
|
return details.userVisibleState === 'error';
|
|
}
|
|
return details.delivered === false;
|
|
}
|
|
|
|
export function shouldClearPendingReplyForOpenCodeRuntimeDelivery(
|
|
runtimeDelivery: SendMessageResult['runtimeDelivery'] | null | undefined
|
|
): boolean {
|
|
if (runtimeDelivery?.attempted !== true) {
|
|
return false;
|
|
}
|
|
const userVisibleState = runtimeDelivery.userVisibleImpact?.state;
|
|
if (userVisibleState === 'none') {
|
|
return true;
|
|
}
|
|
if (userVisibleState === 'warning' || userVisibleState === 'error') {
|
|
return true;
|
|
}
|
|
if (userVisibleState === 'checking') {
|
|
return false;
|
|
}
|
|
return runtimeDelivery.responsePending !== true;
|
|
}
|
|
|
|
export function formatOpenCodeRuntimeDeliveryDebugDetails(
|
|
details: OpenCodeRuntimeDeliveryDebugDetails
|
|
): string {
|
|
return JSON.stringify(
|
|
{
|
|
messageId: details.messageId,
|
|
statusMessageId: details.statusMessageId,
|
|
providerId: details.providerId,
|
|
delivered: details.delivered,
|
|
responsePending: details.responsePending,
|
|
responseState: details.responseState,
|
|
ledgerStatus: details.ledgerStatus,
|
|
visibleReplyMessageId: details.visibleReplyMessageId,
|
|
visibleReplyCorrelation: details.visibleReplyCorrelation,
|
|
queuedBehindMessageId: details.queuedBehindMessageId,
|
|
acceptanceUnknown: details.acceptanceUnknown,
|
|
reason: details.reason,
|
|
diagnostics: details.diagnostics,
|
|
userVisibleState: details.userVisibleState,
|
|
userVisibleReasonCode: details.userVisibleReasonCode,
|
|
userVisibleMessage: details.userVisibleMessage,
|
|
userVisibleNextReviewAt: details.userVisibleNextReviewAt,
|
|
},
|
|
null,
|
|
2
|
|
);
|
|
}
|