Skip to content

fix: prevent config watcher feedback loop during OAuth token refresh#1059

Open
QiuYi111 wants to merge 1 commit into
cyrusagents:mainfrom
QiuYi111:fix/config-watcher-token-refresh-loop
Open

fix: prevent config watcher feedback loop during OAuth token refresh#1059
QiuYi111 wants to merge 1 commit into
cyrusagents:mainfrom
QiuYi111:fix/config-watcher-token-refresh-loop

Conversation

@QiuYi111
Copy link
Copy Markdown

@QiuYi111 QiuYi111 commented Apr 1, 2026

Summary

Fixes the OAuth token refresh feedback loop and concurrent token exhaustion that forces users to repeatedly re-authenticate.

Three changes across two files:

1. Update in-memory config before writing to disk (EdgeWorker.saveOAuthTokens)

When the file watcher fires after config.json is updated, it compares in-memory vs on-disk state. By updating in-memory first, no diff is detected, preventing the feedback loop.

2. Cooldown on static refresh coalescing (LinearIssueTrackerService.doTokenRefresh)

Keep the resolved pendingRefreshes promise for 5 seconds instead of clearing immediately in finally. Late-arriving 401s coalesce onto the resolved promise instead of triggering redundant HTTP refresh calls that fail because the OAuth refresh token is single-use.

3. Clear stale instance promise on retry failure (LinearIssueTrackerService)

If the retry with a refreshed token also returns 401, clear the stale refreshPromise so the next request can trigger a fresh refresh. Without this, the instance-level coalesce holds a stale promise forever and the token can never be refreshed again after natural expiration (~1 hour).

Problem

When OAuth tokens are refreshed, saveOAuthTokens writes to config.json → chokidar detects the change → handleConfigChange fires → detectGlobalConfigChanges sees linearWorkspaces changed → updateLinearWorkspaceTokens calls setAccessToken() which clears instance-level coalescing → concurrent 401s trigger new refreshes → refresh token already consumed → 400 → token exhaustion → user must re-auth.

Test Plan

  • All 642 existing tests pass (edge-worker)
  • pnpm biome check passes
  • Full monorepo typecheck passes
  • Tested locally: OAuth token refresh no longer triggers config watcher loop
  • Tested locally: ran for hours without needing to re-authenticate (previously required re-auth every ~1 hour)

🤖 Generated with Claude Code

@Connoropolous
Copy link
Copy Markdown
Contributor

instead of that, we propose to first change the in memory, then to update the file.

the lack of differences between the in memory and the file will mean no further restarts or cyclical change loop

@Connoropolous
Copy link
Copy Markdown
Contributor

can you give this a try and let us know if that holds up for you?

@QiuYi111 QiuYi111 force-pushed the fix/config-watcher-token-refresh-loop branch from c743454 to 4013cf0 Compare April 2, 2026 02:17
@QiuYi111
Copy link
Copy Markdown
Author

QiuYi111 commented Apr 2, 2026

Great suggestion! Updated the approach: we now update in-memory config first via configManager.setConfig(), then write to disk. When the file watcher fires, it sees no diff between in-memory and on-disk state, so no feedback loop — without needing to pause/resume the watcher.

This is cleaner and simpler. All 642 tests pass.

…xhaustion

Three changes to prevent the OAuth token refresh cycle that forces
users to repeatedly re-authenticate:

1. EdgeWorker.saveOAuthTokens: update in-memory config BEFORE writing
   to disk. When the config watcher fires, it sees no diff between
   in-memory and on-disk state, so no feedback loop is triggered.

2. LinearIssueTrackerService.doTokenRefresh: keep the resolved
   pendingRefreshes promise for 5 seconds (cooldown) instead of
   clearing immediately in finally. Late-arriving 401s coalesce onto
   the resolved promise instead of triggering redundant HTTP refresh
   calls that would fail because the refresh token is single-use.

3. LinearIssueTrackerService retry catch: if the retry with a
   refreshed token also returns 401, clear the stale refreshPromise
   so the next request can trigger a fresh refresh. Without this, the
   instance-level coalesce holds a stale promise forever and the token
   can never be refreshed again after natural expiration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@QiuYi111 QiuYi111 force-pushed the fix/config-watcher-token-refresh-loop branch from 9ee340d to eb681d8 Compare April 2, 2026 07:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants