fix(bluesky): persist session JWTs and skip retries on rate limits (#583)

Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
Cameron
2026-03-12 21:27:59 -07:00
committed by GitHub
parent 5bed4e78cd
commit 9bd74227d9

View File

@@ -634,6 +634,11 @@ export class BlueskyAdapter implements ChannelAdapter {
return await fn(); return await fn();
} catch (err) { } catch (err) {
lastError = err as Error; lastError = err as Error;
// Don't retry rate limit errors -- back off and let the next poll cycle handle it
if (lastError.message?.includes('RateLimitExceeded')) {
log.warn(`${label} rate-limited. Skipping retries.`);
throw lastError;
}
if (attempt < maxRetries - 1) { if (attempt < maxRetries - 1) {
const delay = Math.min(5000, 1000 * Math.pow(2, attempt)); const delay = Math.min(5000, 1000 * Math.pow(2, attempt));
log.warn(`${label} failed (attempt ${attempt + 1}/${maxRetries}). Retrying in ${delay}ms.`); log.warn(`${label} failed (attempt ${attempt + 1}/${maxRetries}). Retrying in ${delay}ms.`);
@@ -1470,6 +1475,8 @@ export class BlueskyAdapter implements ChannelAdapter {
auth?: { auth?: {
did?: string; did?: string;
handle?: string; handle?: string;
accessJwt?: string;
refreshJwt?: string;
}; };
notificationsCursor?: string; notificationsCursor?: string;
}>; }>;
@@ -1482,13 +1489,21 @@ export class BlueskyAdapter implements ChannelAdapter {
// wantedDids and wantedCollections are NOT restored from state -- config is // wantedDids and wantedCollections are NOT restored from state -- config is
// authoritative. State previously persisted these, but restoring them would // authoritative. State previously persisted these, but restoring them would
// silently override user edits to lettabot.yaml made while the bot was down. // silently override user edits to lettabot.yaml made while the bot was down.
// JWTs are not persisted; session DID and handle are non-secret and safe to store
if (entry?.auth?.did) { if (entry?.auth?.did) {
this.sessionDid = entry.auth.did; this.sessionDid = entry.auth.did;
} }
if (entry?.auth?.handle && entry?.auth?.did) { if (entry?.auth?.handle && entry?.auth?.did) {
this.handleByDid.set(entry.auth.did, entry.auth.handle); this.handleByDid.set(entry.auth.did, entry.auth.handle);
} }
// Restore JWTs so we can refresh instead of re-authenticating on restart
if (entry?.auth?.accessJwt) {
this.accessJwt = entry.auth.accessJwt;
this.accessJwtExpiresAt = decodeJwtExp(entry.auth.accessJwt);
}
if (entry?.auth?.refreshJwt) {
this.refreshJwt = entry.auth.refreshJwt;
this.refreshJwtExpiresAt = decodeJwtExp(entry.auth.refreshJwt);
}
if (entry?.notificationsCursor) { if (entry?.notificationsCursor) {
this.notificationsCursor = entry.notificationsCursor; this.notificationsCursor = entry.notificationsCursor;
this.notificationsInitialized = true; this.notificationsInitialized = true;
@@ -1528,11 +1543,12 @@ export class BlueskyAdapter implements ChannelAdapter {
const agents = typeof existing.agents === 'object' && existing.agents const agents = typeof existing.agents === 'object' && existing.agents
? { ...existing.agents } ? { ...existing.agents }
: {}; : {};
// Only persist non-secret session metadata; JWTs are re-acquired on startup
const auth = this.sessionDid const auth = this.sessionDid
? { ? {
did: this.sessionDid, did: this.sessionDid,
handle: this.config.handle, handle: this.config.handle,
accessJwt: this.accessJwt,
refreshJwt: this.refreshJwt,
} }
: undefined; : undefined;