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