diff --git a/extensions/tlon/src/monitor/index.ts b/extensions/tlon/src/monitor/index.ts index ee74d8a3495..86d993d189b 100644 --- a/extensions/tlon/src/monitor/index.ts +++ b/extensions/tlon/src/monitor/index.ts @@ -103,24 +103,58 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise runtime.log?.(message), - error: (message) => runtime.error?.(message), - }, - }); - } catch (error: any) { - runtime.error?.(`[tlon] Failed to authenticate: ${error?.message ?? String(error)}`); - throw error; + const ssrfPolicy = ssrfPolicyFromAllowPrivateNetwork(account.allowPrivateNetwork); + + // Helper to authenticate with retry logic + async function authenticateWithRetry(maxAttempts = 10): Promise { + for (let attempt = 1; ; attempt++) { + if (opts.abortSignal?.aborted) { + throw new Error("Aborted while waiting to authenticate"); + } + try { + runtime.log?.(`[tlon] Attempting authentication to ${account.url}...`); + return await authenticate(account.url, account.code, { ssrfPolicy }); + } catch (error: any) { + runtime.error?.( + `[tlon] Failed to authenticate (attempt ${attempt}): ${error?.message ?? String(error)}`, + ); + if (attempt >= maxAttempts) { + throw error; + } + const delay = Math.min(30000, 1000 * Math.pow(2, attempt - 1)); + runtime.log?.(`[tlon] Retrying authentication in ${delay}ms...`); + await new Promise((resolve, reject) => { + const timer = setTimeout(resolve, delay); + if (opts.abortSignal) { + const onAbort = () => { + clearTimeout(timer); + reject(new Error("Aborted")); + }; + opts.abortSignal.addEventListener("abort", onAbort, { once: true }); + } + }); + } + } } + let api: UrbitSSEClient | null = null; + const cookie = await authenticateWithRetry(); + api = new UrbitSSEClient(account.url, cookie, { + ship: botShipName, + ssrfPolicy, + logger: { + log: (message) => runtime.log?.(message), + error: (message) => runtime.error?.(message), + }, + // Re-authenticate on reconnect in case the session expired + onReconnect: async (client) => { + runtime.log?.("[tlon] Re-authenticating on SSE reconnect..."); + const newCookie = await authenticateWithRetry(5); + client.updateCookie(newCookie); + runtime.log?.("[tlon] Re-authentication successful"); + }, + }); + const processedTracker = createProcessedMessageTracker(2000); let groupChannels: string[] = []; let botNickname: string | null = null; diff --git a/extensions/tlon/src/urbit/sse-client.ts b/extensions/tlon/src/urbit/sse-client.ts index 972b02c4384..8559e18746b 100644 --- a/extensions/tlon/src/urbit/sse-client.ts +++ b/extensions/tlon/src/urbit/sse-client.ts @@ -341,6 +341,14 @@ export class UrbitSSEClient { ); } + /** + * Update the cookie used for authentication. + * Call this when re-authenticating after session expiry. + */ + updateCookie(newCookie: string): void { + this.cookie = normalizeUrbitCookie(newCookie); + } + private async ack(eventId: number): Promise { this.lastAcknowledgedEventId = eventId;