@@ -69,7 +69,7 @@ class OpencodeApiClient(private val client: HttpClient) {
69 text: String,
69 text: String,
70 model: ModelRef? = null,
70 model: ModelRef? = null,
71 agent: String? = null,
71 agent: String? = null,
72− ): MessageWithParts = withRetry {
72+ ): MessageWithParts =
73 client.post("session/$sessionId/message") {
73 client.post("session/$sessionId/message") {
74 contentType(ContentType.Application.Json)
74 contentType(ContentType.Application.Json)
@@ -80,7 +80,6 @@ class OpencodeApiClient(private val client: HttpClient) {
85 suspend fun abort(sessionId: String) {
84 suspend fun abort(sessionId: String) {
86 client.post("session/$sessionId/abort")
85 client.post("session/$sessionId/abort")
@@ -145,7 +144,7 @@ private suspend fun <T> withRetry(
145 maxAttempts: Int = NetworkConfig.retryMaxAttempts,
144 maxAttempts: Int = NetworkConfig.retryMaxAttempts,
146 initialDelayMs: Long = NetworkConfig.retryInitialDelayMs,
145 initialDelayMs: Long = NetworkConfig.retryInitialDelayMs,
147 block: suspend () -> T,
146 block: suspend () -> T,
148−): T = withRetryInternal(maxAttempts, initialDelayMs, block)
147+): T = withRetryInternal(maxAttempts, initialDelayMs, jitterFactor = NetworkConfig.retryJitterFactor, block)
151 * Retry a suspending block up to [maxAttempts] times with exponential backoff.
150 * Retry a suspending block up to [maxAttempts] times with exponential backoff.
@@ -158,6 +157,7 @@ private suspend fun <T> withRetry(
158internal suspend fun <T> withRetryInternal(
157internal suspend fun <T> withRetryInternal(
159 maxAttempts: Int = NetworkConfig.retryMaxAttempts,
158 maxAttempts: Int = NetworkConfig.retryMaxAttempts,
160 initialDelayMs: Long = NetworkConfig.retryInitialDelayMs,
159 initialDelayMs: Long = NetworkConfig.retryInitialDelayMs,
160+ jitterFactor: Double = NetworkConfig.retryJitterFactor,
161 block: suspend () -> T,
161 block: suspend () -> T,
163 var lastError: Throwable? = null
163 var lastError: Throwable? = null
@@ -169,7 +169,11 @@ internal suspend fun <T> withRetryInternal(
169 } catch (t: Throwable) {
169 } catch (t: Throwable) {
170 lastError = t
170 lastError = t
171 if (t is ClientRequestException) throw t
171 if (t is ClientRequestException) throw t
172+ if (attempt < maxAttempts) {
172− if (attempt < maxAttempts) delay(initialDelayMs * (1 shl (attempt - 1)))
173+ val baseDelay = initialDelayMs * (1 shl (attempt - 1))
174+ val jitter = (baseDelay * jitterFactor * Math.random()).toLong()
175+ delay(baseDelay + jitter)
175 throw lastError ?: IllegalStateException("withRetry failed without error")
179 throw lastError ?: IllegalStateException("withRetry failed without error")