* 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, )
| 1642 | * Ensures the server learns the work item ended, preventing server-side zombies. |
| 1643 | */ |
| 1644 | async function stopWorkWithRetry( |
| 1645 | api: BridgeApiClient, |
| 1646 | environmentId: string, |
| 1647 | workId: string, |
| 1648 | logger: BridgeLogger, |
| 1649 | baseDelayMs = 1000, |
| 1650 | ): Promise<void> { |
| 1651 | const MAX_ATTEMPTS = 3 |
| 1652 | |
| 1653 | for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { |
| 1654 | try { |
| 1655 | await api.stopWork(environmentId, workId, false) |
| 1656 | logForDebugging( |
| 1657 | `[bridge:work] stopWork succeeded for workId=${workId} on attempt ${attempt}/${MAX_ATTEMPTS}`, |
| 1658 | ) |
| 1659 | return |
| 1660 | } catch (err) { |
| 1661 | // Auth/permission errors won't be fixed by retrying |
| 1662 | if (err instanceof BridgeFatalError) { |
| 1663 | if (isSuppressible403(err)) { |
| 1664 | logForDebugging( |
| 1665 | `[bridge:work] Suppressed stopWork 403 for ${workId}: ${err.message}`, |
| 1666 | ) |
| 1667 | } else { |
| 1668 | logger.logError(`Failed to stop work ${workId}: ${err.message}`) |
| 1669 | } |
| 1670 | logForDiagnosticsNoPII('error', 'bridge_stop_work_failed', { |
| 1671 | attempts: attempt, |
| 1672 | fatal: true, |
| 1673 | }) |
| 1674 | return |
| 1675 | } |
| 1676 | const errMsg = errorMessage(err) |
| 1677 | if (attempt < MAX_ATTEMPTS) { |
| 1678 | const delay = addJitter(baseDelayMs * Math.pow(2, attempt - 1)) |
| 1679 | logger.logVerbose( |
| 1680 | `Failed to stop work ${workId} (attempt ${attempt}/${MAX_ATTEMPTS}), retrying in ${formatDelay(delay)}: ${errMsg}`, |
| 1681 | ) |
| 1682 | await sleep(delay) |
| 1683 | } else { |
| 1684 | logger.logError( |
| 1685 | `Failed to stop work ${workId} after ${MAX_ATTEMPTS} attempts: ${errMsg}`, |
| 1686 | ) |
| 1687 | logForDiagnosticsNoPII('error', 'bridge_stop_work_failed', { |
| 1688 | attempts: MAX_ATTEMPTS, |
| 1689 | }) |
| 1690 | } |
| 1691 | } |
| 1692 | } |
| 1693 | } |
| 1694 | |
| 1695 | function onSessionTimeout( |
| 1696 | sessionId: string, |
no test coverage detected