* Retry stopWork with exponential backoff (3 attempts, 1s/2s/4s). * Ensures the server learns the work item ended, preventing server-side zombies.
( api: BridgeApiClient, environmentId: string, workId: string, logger: BridgeLogger, baseDelayMs = 1000, )
| 1625 | * Ensures the server learns the work item ended, preventing server-side zombies. |
| 1626 | */ |
| 1627 | async function stopWorkWithRetry( |
| 1628 | api: BridgeApiClient, |
| 1629 | environmentId: string, |
| 1630 | workId: string, |
| 1631 | logger: BridgeLogger, |
| 1632 | baseDelayMs = 1000, |
| 1633 | ): Promise<void> { |
| 1634 | const MAX_ATTEMPTS = 3 |
| 1635 | |
| 1636 | for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { |
| 1637 | try { |
| 1638 | await api.stopWork(environmentId, workId, false) |
| 1639 | logForDebugging( |
| 1640 | `[bridge:work] stopWork succeeded for workId=${workId} on attempt ${attempt}/${MAX_ATTEMPTS}`, |
| 1641 | ) |
| 1642 | return |
| 1643 | } catch (err) { |
| 1644 | // Auth/permission errors won't be fixed by retrying |
| 1645 | if (err instanceof BridgeFatalError) { |
| 1646 | if (isSuppressible403(err)) { |
| 1647 | logForDebugging( |
| 1648 | `[bridge:work] Suppressed stopWork 403 for ${workId}: ${err.message}`, |
| 1649 | ) |
| 1650 | } else { |
| 1651 | logger.logError(`Failed to stop work ${workId}: ${err.message}`) |
| 1652 | } |
| 1653 | logForDiagnosticsNoPII('error', 'bridge_stop_work_failed', { |
| 1654 | attempts: attempt, |
| 1655 | fatal: true, |
| 1656 | }) |
| 1657 | return |
| 1658 | } |
| 1659 | const errMsg = errorMessage(err) |
| 1660 | if (attempt < MAX_ATTEMPTS) { |
| 1661 | const delay = addJitter(baseDelayMs * Math.pow(2, attempt - 1)) |
| 1662 | logger.logVerbose( |
| 1663 | `Failed to stop work ${workId} (attempt ${attempt}/${MAX_ATTEMPTS}), retrying in ${formatDelay(delay)}: ${errMsg}`, |
| 1664 | ) |
| 1665 | await sleep(delay) |
| 1666 | } else { |
| 1667 | logger.logError( |
| 1668 | `Failed to stop work ${workId} after ${MAX_ATTEMPTS} attempts: ${errMsg}`, |
| 1669 | ) |
| 1670 | logForDiagnosticsNoPII('error', 'bridge_stop_work_failed', { |
| 1671 | attempts: MAX_ATTEMPTS, |
| 1672 | }) |
| 1673 | } |
| 1674 | } |
| 1675 | } |
| 1676 | } |
| 1677 | |
| 1678 | function onSessionTimeout( |
| 1679 | sessionId: string, |
no test coverage detected