fix(tlon): restore auth retry and add reauth on SSE reconnect

- Add authenticateWithRetry() helper with exponential backoff (restores lost logic from #39)
- Add onReconnect callback to re-authenticate when SSE stream reconnects
- Add UrbitSSEClient.updateCookie() method for proper cookie normalization on reauth
This commit is contained in:
Hunter Miller
2026-02-19 15:12:11 -06:00
committed by Josh Lehman
parent 01715182e1
commit 94501d3310
2 changed files with 58 additions and 16 deletions

View File

@@ -103,24 +103,58 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
const botShipName = normalizeShip(account.ship);
runtime.log?.(`[tlon] Starting monitor for ${botShipName}`);
let api: UrbitSSEClient | null = null;
try {
const ssrfPolicy = ssrfPolicyFromAllowPrivateNetwork(account.allowPrivateNetwork);
runtime.log?.(`[tlon] Attempting authentication to ${account.url}...`);
const cookie = await authenticate(account.url, account.code, { ssrfPolicy });
api = new UrbitSSEClient(account.url, cookie, {
ship: botShipName,
ssrfPolicy,
logger: {
log: (message) => 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<string> {
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<void>((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;

View File

@@ -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<void> {
this.lastAcknowledgedEventId = eventId;