Skip to content

fix: ensure Qwen refreshed tokens are persisted to disk#2726

Open
aeddk-bnh wants to merge 1 commit intorouter-for-me:devfrom
Sotatek-HieuBui2:fix/qwen-token-persistence
Open

fix: ensure Qwen refreshed tokens are persisted to disk#2726
aeddk-bnh wants to merge 1 commit intorouter-for-me:devfrom
Sotatek-HieuBui2:fix/qwen-token-persistence

Conversation

@aeddk-bnh
Copy link
Copy Markdown

Summary

Updated QwenExecutor.Refresh to synchronize the new access_token and rotated refresh_token into the underlying storage object. This ensures that FileTokenStore correctly persists the latest credentials to disk, preventing "invalid token" errors after server restarts or cache misses.

Key Changes

  • Synchronize refreshed token data (Access/Refresh/Expiry) to auth.Storage.
  • Injected refreshTokens function to QwenExecutor for easier unit testing.
  • Added comprehensive unit tests in qwen_executor_test.go to verify storage updates and graceful fallback when storage is nil.

Test plan

  • Ran go test -v ./internal/runtime/executor/qwen_executor_test.go - PASS.
  • Verified that Metadata is updated correctly.
  • Verified that QwenTokenStorage struct is updated when present.
  • Verified that the executor doesn't panic when auth.Storage is nil.

🤖 Generated with Claude Code

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enhances the QwenExecutor by ensuring that refreshed tokens are persisted to the underlying QwenTokenStorage and introduces dependency injection for the token refresh process to improve testability. New unit tests have been added to verify these updates. Key feedback includes a recommendation to update the LastRefreshedAt and UpdatedAt fields on the Auth struct for consistency, and a warning regarding potential data loss in UpdateTokenStorage if the refresh response contains empty fields.

// Update the underlying storage object so FileTokenStore persists the
// refreshed tokens (especially the rotated refresh_token) to disk.
if storage, ok := auth.Storage.(*qwenauth.QwenTokenStorage); ok {
svc.UpdateTokenStorage(storage, td)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The UpdateTokenStorage method (defined in internal/auth/qwen/qwen_auth.go) appears to unconditionally overwrite storage.RefreshToken and storage.ResourceURL. If the refresh response from the server does not include these fields (which is common if the refresh token has not rotated), the existing credentials in storage will be wiped. This will prevent subsequent refreshes once the current access token expires. Please update UpdateTokenStorage to only update these fields if they are non-empty in the td object, matching the logic used for auth.Metadata in lines 667-672.

Comment on lines 676 to 677
now := time.Now().Format(time.RFC3339)
auth.Metadata["last_refresh"] = now
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In addition to updating the metadata, it is important to update the LastRefreshedAt and UpdatedAt fields on the Auth struct itself. This ensures that the system's internal state is correctly maintained for components that rely on these timestamps (such as the AuthManager or UI). Using UTC is recommended for consistency with the struct's documentation.

Suggested change
now := time.Now().Format(time.RFC3339)
auth.Metadata["last_refresh"] = now
now := time.Now().UTC()
auth.Metadata["last_refresh"] = now.Format(time.RFC3339)
auth.LastRefreshedAt = now
auth.UpdatedAt = now

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ee288f50f8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +681 to +682
if storage, ok := auth.Storage.(*qwenauth.QwenTokenStorage); ok {
svc.UpdateTokenStorage(storage, td)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve optional token fields when syncing Qwen storage

Refresh now always calls svc.UpdateTokenStorage(storage, td), but that helper unconditionally assigns RefreshToken and ResourceURL; this conflicts with the guarded metadata updates just above (td.RefreshToken != "", td.ResourceURL != "") and can erase previously valid values when the refresh response omits optional fields. In deployments using GitTokenStore/PostgresStore/ObjectTokenStore (which persist auth.Storage directly), this can write an empty refresh_token to disk and cause the next refresh cycle to fail after restart or cache miss.

Useful? React with 👍 / 👎.

@aeddk-bnh aeddk-bnh force-pushed the fix/qwen-token-persistence branch from ee288f5 to bf70c15 Compare April 12, 2026 06:44
@aeddk-bnh
Copy link
Copy Markdown
Author

Thank you for the detailed feedback! I've updated the implementation to address the concerns:

  • Preserving Data: Updated UpdateTokenStorage to only overwrite RefreshToken and ResourceURL if the new values are non-empty. This prevents existing credentials from being wiped when the server omits optional fields during refresh token rotation.
  • Synchronized Auth Timestamps: Added updates for auth.LastRefreshedAt and auth.UpdatedAt using time.Now().UTC() to ensure the system's internal state is consistent.
  • New Unit Test: Added TestQwenExecutorRefresh_PreservesExistingStorageWhenResponseHasEmptyFields to verify that old storage values are correctly preserved when the refresh response contains empty optional fields.

Force-pushed the updated commit.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bf70c15375

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +683 to +684
if storage, ok := auth.Storage.(*qwenauth.QwenTokenStorage); ok {
svc.UpdateTokenStorage(storage, td)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid mutating shared storage from request refresh

Refresh runs on per-request auth clones, but Auth.Clone() only shallow-copies Storage, so this write mutates the same *QwenTokenStorage instance held by the manager. In concurrent traffic (e.g., two requests on one credential where one hits 429 and refreshes while another is being persisted via MarkResult), this unsynchronized cross-goroutine mutation can race with store serialization and persist inconsistent token state. Keep refresh mutations request-local (or deep-copy/synchronize storage updates) before writing through the shared pointer.

Useful? React with 👍 / 👎.

Updated QwenExecutor.Refresh to synchronize the new access_token and
rotated refresh_token into the underlying storage object. This ensures
that FileTokenStore correctly persists the latest credentials to disk,
preventing "invalid token" errors after server restarts or cache misses.

Added unit tests to verify both Metadata and Storage updates.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@aeddk-bnh aeddk-bnh force-pushed the fix/qwen-token-persistence branch from bf70c15 to c225da9 Compare April 12, 2026 06:58
Copy link
Copy Markdown
Collaborator

@luispater luispater left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary:
This PR improves the local QwenExecutor.Refresh() state update logic, but it still does not fix the persistence path described in the PR.

Blocking findings:

  • The 429 immediate-retry flow refreshes only the cloned Auth passed into executor.Execute(). That refreshed copy is never written back through Manager.Update(), so the latest tokens are still not persisted for the long-lived manager state or for restart recovery.
  • After restart, FileTokenStore.readAuthFile() reconstructs Qwen auths from Metadata only and leaves Storage unset. Updating/cloning auth.Storage inside QwenExecutor.Refresh() therefore does not address the restart/cache-miss scenario called out in the PR description.

Non-blocking findings:

  • FileTokenStore.Save() already calls SetMetadata(auth.Metadata), and MergeMetadata() applies metadata after serializing the storage struct. That means metadata already wins during file writes, so the new storage mutation is not the critical missing piece.
  • The new tests validate executor-local storage mutation, but they do not cover the manager/store integration path where persistence actually happens.

Test plan:

  • Not run; review based on code inspection of the refresh, manager, and filestore paths.

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.

3 participants