Problem
TokenManager.getBotToken() and TokenManager.getGraphToken() call MSAL's acquireTokenByClientCredential({ scopes }) without passing skipCache. This means callers have no way to force a fresh token after receiving a 401 from the Bot Framework or Graph API.
MSAL caches tokens with a ~5 minute buffer before expiry. A token can expire between the moment it's fetched from cache and when the HTTP response arrives — particularly during:
- Long-running operations (file uploads to OneDrive/SharePoint)
- Streaming sessions that span minutes (Teams streaminfo protocol)
- Proactive messaging that happens long after the initial token was cached
When this race condition occurs, the caller gets a 401 and wants to retry with a fresh token. But calling getBotToken() / getGraphToken() again returns the same cached token because MSAL still considers it valid.
Proposed Solution
Add an optional skipCache parameter to getBotToken and getGraphToken that threads through to MSAL's acquireTokenByClientCredential({ scopes, skipCache }):
// token-manager.ts
async getBotToken(skipCache?: boolean): Promise<IToken | null> { ... }
async getGraphToken(tenantId?: string, skipCache?: boolean): Promise<IToken | null> { ... }
// app.ts
protected async getBotToken(skipCache?: boolean) { ... }
protected async getAppGraphToken(tenantId?: string, skipCache?: boolean) { ... }
This is fully backward-compatible — skipCache defaults to false, so all existing callers are unaffected. Only 401-retry code paths would pass true.
Context
We hit this in the OpenClaw Teams plugin where Bot Framework REST calls (update/delete activity) and Graph API calls (OneDrive uploads, sharing links, chat member lookups) intermittently fail with 401 after the bot has been running for extended periods. We implemented a retry-on-401 pattern but it's ineffective without skipCache because the retry gets the same stale token from MSAL's cache.
MSAL's ClientCredentialRequest already supports skipCache?: boolean — the Teams SDK just doesn't expose it.
Problem
TokenManager.getBotToken()andTokenManager.getGraphToken()call MSAL'sacquireTokenByClientCredential({ scopes })without passingskipCache. This means callers have no way to force a fresh token after receiving a 401 from the Bot Framework or Graph API.MSAL caches tokens with a ~5 minute buffer before expiry. A token can expire between the moment it's fetched from cache and when the HTTP response arrives — particularly during:
When this race condition occurs, the caller gets a 401 and wants to retry with a fresh token. But calling
getBotToken()/getGraphToken()again returns the same cached token because MSAL still considers it valid.Proposed Solution
Add an optional
skipCacheparameter togetBotTokenandgetGraphTokenthat threads through to MSAL'sacquireTokenByClientCredential({ scopes, skipCache }):This is fully backward-compatible —
skipCachedefaults tofalse, so all existing callers are unaffected. Only 401-retry code paths would passtrue.Context
We hit this in the OpenClaw Teams plugin where Bot Framework REST calls (update/delete activity) and Graph API calls (OneDrive uploads, sharing links, chat member lookups) intermittently fail with 401 after the bot has been running for extended periods. We implemented a retry-on-401 pattern but it's ineffective without
skipCachebecause the retry gets the same stale token from MSAL's cache.MSAL's
ClientCredentialRequestalready supportsskipCache?: boolean— the Teams SDK just doesn't expose it.