{"record":{"id":"bb12ef11555fc27c","repo":"can1357/oh-my-pi","slug":"oauth-refresh-ownership-was-lost-before-persistenc","errorCode":null,"errorMessage":"OAuth refresh ownership was lost before persistence","messagePattern":"OAuth refresh ownership was lost before persistence","errorType":"exception","errorClass":"AIError.ConfigurationError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/auth-storage.ts","lineNumber":2567,"sourceCode":"\t\t\tconst serialized = serializeCredential(provider, current);\n\t\t\tif (!serialized) return { credential: current, refreshed: false, removed: false };\n\n\t\t\tlet stopLeaseRenewal = false;\n\t\t\tlet leaseRenewalError: unknown;\n\t\t\tconst leaseRenewalStopped = Promise.withResolvers<void>();\n\t\t\tconst leaseRenewal =\n\t\t\t\tleasedCredentialId !== undefined\n\t\t\t\t\t? (async () => {\n\t\t\t\t\t\t\twhile (!stopLeaseRenewal) {\n\t\t\t\t\t\t\t\tawait Promise.race([Bun.sleep(OAUTH_REFRESH_LEASE_RENEW_MS), leaseRenewalStopped.promise]);\n\t\t\t\t\t\t\t\tif (stopLeaseRenewal) return;\n\t\t\t\t\t\t\t\tconst renewed = this.#store.renewCredentialRefreshLease?.(\n\t\t\t\t\t\t\t\t\tleasedCredentialId,\n\t\t\t\t\t\t\t\t\towner,\n\t\t\t\t\t\t\t\t\tDate.now() + OAUTH_REFRESH_LEASE_TTL_MS,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tif (!renewed) {\n\t\t\t\t\t\t\t\t\tthrow new AIError.ConfigurationError(\"OAuth refresh ownership was lost before persistence\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t})().catch(error => {\n\t\t\t\t\t\t\tleaseRenewalError = error;\n\t\t\t\t\t\t})\n\t\t\t\t\t: undefined;\n\t\t\tconst refreshAbort = new AbortController();\n\t\t\tconst refreshTimeout = setTimeout(() => {\n\t\t\t\trefreshAbort.abort(\n\t\t\t\t\tnew AIError.OAuthError(`OAuth token refresh timed out for provider: ${provider}`, {\n\t\t\t\t\t\tkind: \"timeout\",\n\t\t\t\t\t\tprovider,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}, options.refreshTimeoutMs ?? OAUTH_REFRESH_OPERATION_TIMEOUT_MS);\n\n\t\t\tlet refreshed: OAuthCredentials;\n\t\t\ttry {","sourceCodeStart":2549,"sourceCodeEnd":2585,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/auth-storage.ts#L2549-L2585","documentation":"While refreshing, the lease owner periodically renews its durable refresh lease (renewCredentialRefreshLease) to keep ownership until persistence. If a renewal returns false — meaning the lease was taken over or expired before the new token was persisted — the library throws ConfigurationError because persisting under lost ownership could clobber another process's fresher credential.","triggerScenarios":"The OAuth refresh takes longer than OAUTH_REFRESH_LEASE_TTL_MS and the renewal call fails to extend the lease — typically because another process acquired the lease after expiry, or the backing store lost/reset the lease row mid-refresh.","commonSituations":"Very slow token endpoint or network causing the refresh to exceed lease TTL; multiple machines sharing the same credential database racing to refresh; a store implementation whose lease renewal misbehaves or whose clock is skewed.","solutions":["Retry the refresh — after losing the lease, the other owner likely stored a fresh credential; re-read the stored credential first","Check for clock skew between processes sharing the store, since lease expiry uses wall-clock timestamps","Reduce refresh latency (network issues) so renewal happens well within the lease TTL","If persistent, verify the store's tryAcquire/renewCredentialRefreshLease implementation honors owner semantics"],"exampleFix":"// before\nawait refreshAndPersist(); // throws if lease lost mid-flight\n// after\ntry {\n\tawait refreshAndPersist();\n} catch (error) {\n\tif (error instanceof AIError.ConfigurationError && /ownership was lost/.test(error.message)) {\n\t\treturn storage.getOAuthAccess(provider); // peer likely persisted a fresh token\n\t}\n\tthrow error;\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n\tawait doRefresh();\n} catch (error) {\n\tif (error instanceof AIError.ConfigurationError && /ownership was lost/.test(error.message)) {\n\t\tconst fresh = await storage.getOAuthAccess(provider); // peer likely persisted\n\t\tif (fresh) return fresh;\n\t}\n\tthrow error;\n}","preventionTips":["Keep refresh operations fast (avoid slow pre-work inside the lease) so renewals stay well within TTL","Synchronize clocks (NTP) across machines sharing the credential store","Avoid many processes force-refreshing the same credential simultaneously"],"tags":["oauth","lease","concurrency","race-condition"],"backgroundTag":"refresh-lease-lost","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}