* Heartbeat all active work items. * Returns 'ok' if at least one heartbeat succeeded, 'auth_failed' if any * got a 401/403 (JWT expired — re-queued via reconnectSession so the next * poll delivers fresh work), or 'failed' if all failed for other reasons.
()
| 209 | * poll delivers fresh work), or 'failed' if all failed for other reasons. |
| 210 | */ |
| 211 | async function heartbeatActiveWorkItems(): Promise< |
| 212 | 'ok' | 'auth_failed' | 'fatal' | 'failed' |
| 213 | > { |
| 214 | let anySuccess = false |
| 215 | let anyFatal = false |
| 216 | const authFailedSessions: string[] = [] |
| 217 | for (const [sessionId] of activeSessions) { |
| 218 | const workId = sessionWorkIds.get(sessionId) |
| 219 | const ingressToken = sessionIngressTokens.get(sessionId) |
| 220 | if (!workId || !ingressToken) { |
| 221 | continue |
| 222 | } |
| 223 | try { |
| 224 | await api.heartbeatWork(environmentId, workId, ingressToken) |
| 225 | anySuccess = true |
| 226 | } catch (err) { |
| 227 | logForDebugging( |
| 228 | `[bridge:heartbeat] Failed for sessionId=${sessionId} workId=${workId}: ${errorMessage(err)}`, |
| 229 | ) |
| 230 | if (err instanceof BridgeFatalError) { |
| 231 | logEvent('ncode_bridge_heartbeat_error', { |
| 232 | status: |
| 233 | err.status as unknown as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| 234 | error_type: (err.status === 401 || err.status === 403 |
| 235 | ? 'auth_failed' |
| 236 | : 'fatal') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| 237 | }) |
| 238 | if (err.status === 401 || err.status === 403) { |
| 239 | authFailedSessions.push(sessionId) |
| 240 | } else { |
| 241 | // 404/410 = environment expired or deleted — no point retrying |
| 242 | anyFatal = true |
| 243 | } |
| 244 | } |
| 245 | } |
| 246 | } |
| 247 | // JWT expired → trigger server-side re-dispatch. Without this, work stays |
| 248 | // ACK'd out of the Redis PEL and poll returns empty forever (CC-1263). |
| 249 | // The existingHandle path below delivers the fresh token to the child. |
| 250 | // sessionId is already in the format /bridge/reconnect expects: it comes |
| 251 | // from work.data.id, which matches the server's EnvironmentInstance store |
| 252 | // (cse_* under the compat gate, session_* otherwise). |
| 253 | for (const sessionId of authFailedSessions) { |
| 254 | logger.logVerbose( |
| 255 | `Session ${sessionId} token expired — re-queuing via bridge/reconnect`, |
| 256 | ) |
| 257 | try { |
| 258 | await api.reconnectSession(environmentId, sessionId) |
| 259 | logForDebugging( |
| 260 | `[bridge:heartbeat] Re-queued sessionId=${sessionId} via bridge/reconnect`, |
| 261 | ) |
| 262 | } catch (err) { |
| 263 | logger.logError( |
| 264 | `Failed to refresh session ${sessionId} token: ${errorMessage(err)}`, |
| 265 | ) |
| 266 | logForDebugging( |
| 267 | `[bridge:heartbeat] reconnectSession(${sessionId}) failed: ${errorMessage(err)}`, |
| 268 | { level: 'error' }, |
no test coverage detected