Skip to content
Source
Sign inSign up

UserFacingErrors.ts

146 lines6.8 KB

1import { PROVIDER_DISPLAY_NAMES, type ProviderDriverKind } from "@t3tools/contracts";
2import * as Predicate from "effect/Predicate";
3import * as SqlError from "effect/sql/SqlError";
4​
5const GENERIC_ERROR_PREFIXES = [
6 "Failed to dispatch orchestration V2 command",
7 "Failed to dispatch orchestration command ",
8 "Provider adapter failed while dispatching orchestration command ",
9];
10​
11const SQLITE_RECOVERY_MESSAGES: Readonly<Record<number, string>> = {
12 5: "The T3 Code server database is busy (SQLITE_BUSY). Wait a moment, then retry. If this keeps happening, check for another T3 Code process using the same data directory on the server.",
13 6: "The T3 Code server database is locked (SQLITE_LOCKED). Wait a moment, then retry. If this keeps happening, check the server logs for the process holding the database lock.",
14 8: "The T3 Code server database is read-only (SQLITE_READONLY). On the machine running the server, check that its data directory is writable and its disk is mounted read-write, then retry.",
15 10: "The T3 Code server could not read or write its database (SQLITE_IOERR). On the machine running the server, check free disk space and disk health, then retry. If it still fails, check the server logs.",
16 11: "The T3 Code server database appears damaged (SQLITE_CORRUPT). Preserve the server data directory and check the server logs before restoring a database backup.",
17 13: "The T3 Code server could not save this change because its database or disk is full (SQLITE_FULL). Free space on the machine running the server, then retry. If space is available, check the server logs for a database size limit.",
18 14: "The T3 Code server could not open a database file (SQLITE_CANTOPEN). On the machine running the server, check free disk space and that its data and temporary directories exist and are writable, then retry. If it still fails, check the server logs.",
19 26: "The T3 Code server could not read its database file (SQLITE_NOTADB). Preserve the server data directory and check the configured database path and server logs before restoring a database backup.",
20};
21​
22function databaseErrorMessage(value: unknown): string | undefined {
23 if (!SqlError.isSqlError(value)) return undefined;
24​
25 let cause: unknown = value.cause;
26 const seen = new Set<unknown>();
27 while (Predicate.isObject(cause) && !seen.has(cause)) {
28 seen.add(cause);
29 // node:sqlite uses extended result codes; their low byte is the primary code.
30 if (typeof cause.errcode === "number" && Number.isInteger(cause.errcode) && cause.errcode > 0) {
31 const message = SQLITE_RECOVERY_MESSAGES[cause.errcode & 0xff];
32 if (message !== undefined) return message;
33 }
34 cause = cause.cause;
35 }
36​
37 return "The T3 Code server could not complete this database request. Retry once; if it still fails, check the server logs for the database error.";
39​
40export function userFacingDatabaseErrorMessage(cause: unknown): string | undefined {
41 const seen = new Set<unknown>();
42 let value = cause;
43 while (Predicate.isObject(value) && !seen.has(value)) {
44 seen.add(value);
45 const message = databaseErrorMessage(value);
46 if (message !== undefined) return message;
47 value = value.cause;
48 }
49 return undefined;
51​
52const CAPABILITY_REJECTION_MESSAGES: Record<string, (provider: string) => string> = {
53 queued_messages: (p) => `${p} cannot queue messages behind an active run.`,
54 active_steering: (p) =>
55 `${p} cannot redirect an active run. Stop it first, then send the message.`,
56 interrupt_restart_steering: (p) =>
57 `${p} cannot redirect an active run. Stop it first, then send the message.`,
58 interrupt: (p) => `${p} cannot stop a run once it has started.`,
59 native_fork: (p) => `${p} cannot fork this thread natively.`,
60 fork_from_turn: (p) => `${p} cannot fork from an earlier point in this thread.`,
61 rollback: (p) =>
62 `${p} cannot rewind its conversation, so this checkpoint cannot be restored on this thread.`,
63 rollback_snapshot: (p) =>
64 `${p} did not report its rewound conversation state, so the checkpoint was not restored.`,
65 context_handoff: (p) => `${p} cannot receive the context handoff needed for this switch.`,
66 strong_terminal_status: (p) =>
67 `${p} cannot confirm when its runs finish reliably enough for this.`,
68};
69​
70function providerDisplayName(instanceId: string): string {
71 const known = PROVIDER_DISPLAY_NAMES[instanceId as ProviderDriverKind];
72 if (known !== undefined) return known;
73 const trimmed = instanceId.replace(/Agent$/i, "").trim();
74 if (trimmed.length === 0) return instanceId;
75 return trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
77​
78function policyRejectionMessage(value: unknown): string | undefined {
79 if (!Predicate.isObject(value) || typeof value.providerInstanceId !== "string") return undefined;
80 const provider = providerDisplayName(value.providerInstanceId);
81 if (value._tag === "CommandPolicyCapabilityUnsupportedError") {
82 const capability = typeof value.capability === "string" ? value.capability : "";
83 return CAPABILITY_REJECTION_MESSAGES[capability]?.(provider);
84 }
85 return undefined;
87​
88function textValue(value: unknown): string | undefined {
89 return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
91​
92function messageFrom(value: unknown): string | undefined {
93 if (typeof value === "string") {
94 return textValue(value);
95 }
96 const friendly = policyRejectionMessage(value);
97 if (friendly !== undefined) {
98 return friendly;
99 }
100 if (value instanceof Error) {
101 return textValue(value.message);
102 }
103 if (!Predicate.isObject(value)) {
104 return undefined;
105 }
106 return textValue(value.detail) ?? textValue(value.message);
108​
109function isGenericErrorMessage(message: string): boolean {
110 return GENERIC_ERROR_PREFIXES.some((prefix) => message.startsWith(prefix));
112​
113function collectErrorMessages(value: unknown, seen: Set<unknown>): ReadonlyArray<string> {
114 if (value === undefined || value === null || seen.has(value)) {
115 return [];
116 }
117 seen.add(value);
118​
119 if (Array.isArray(value)) {
120 return value.flatMap((item) => collectErrorMessages(item, seen));
121 }
122​
123 const databaseMessage = databaseErrorMessage(value);
124 if (databaseMessage !== undefined) return [databaseMessage];
125​
126 const message = messageFrom(value);
127 if (!Predicate.isObject(value)) {
128 return message === undefined ? [] : [message];
129 }
130​
131 const nested = [
132 ...collectErrorMessages(value.cause, seen),
133 ...collectErrorMessages(value.error, seen),
134 ...collectErrorMessages(value.errors, seen),
135 ];
136​
137 return message === undefined ? nested : [message, ...nested];
139​
140export function userFacingDispatchErrorMessage(cause: unknown): string | undefined {
141 const messages = collectErrorMessages(cause, new Set()).filter(
142 (message, index, allMessages) =>
143 !isGenericErrorMessage(message) && allMessages.indexOf(message) === index,
144 );
145 return messages.at(-1);