11import { createLogger } from '@sim/logger'
22import { getErrorMessage } from '@sim/utils/errors'
3- import { sleep } from '@sim/utils/helpers'
3+ import { interruptibleSleep } from '@sim/utils/helpers'
4+ import { type BackoffOptions , backoffWithJitter } from '@sim/utils/retry'
45import { truncate } from '@sim/utils/string'
56import { env } from '@/lib/core/config/env'
67import { isMothershipBenchmarkEnabled } from '@/lib/core/config/env-flags'
@@ -22,6 +23,25 @@ import { executeSimControl } from '@/lib/mothership/transport/control'
2223const logger = createLogger ( 'MothershipTransportReceiver' )
2324const receivers = new Map < string , AbortController > ( )
2425
26+ /**
27+ * A lost connection or a worker deploy: retry within a second, slowing to 4 s, so with jitter a
28+ * healthy deployment is back inside the worker's 5 s run-control deadline.
29+ */
30+ const TRANSIENT_BACKOFF : BackoffOptions = { baseMs : 1000 , maxMs : 4000 }
31+ /**
32+ * The worker rejected this deployment's key, which only an operator can fix: start at 30 s and
33+ * slow to 10 minutes, so a misconfigured deployment polls a few hundred times a day, not every
34+ * second, and still reconnects on its own once the key is accepted.
35+ */
36+ const REFUSED_BACKOFF : BackoffOptions = { baseMs : 30_000 , maxMs : 600_000 }
37+
38+ /** The worker refused this deployment's key (401/403), as opposed to failing to answer. */
39+ class TransportKeyRefused extends Error {
40+ constructor ( readonly status : number ) {
41+ super ( `Transport key refused (HTTP ${ status } )` )
42+ }
43+ }
44+
2545interface ReceivedControl {
2646 id : string
2747 answer : ( ) => Promise < SimControlResult >
@@ -61,6 +81,8 @@ export async function receiveSimControls(
6181 channelId : string ,
6282 signal : AbortSignal
6383) : Promise < void > {
84+ let failures = 0
85+ let outage : 'disconnected' | 'key_refused' | undefined
6486 while ( ! signal . aborted ) {
6587 let phase : 'poll' | 'parse' | 'reply' = 'poll'
6688 try {
@@ -75,8 +97,15 @@ export async function receiveSimControls(
7597 } )
7698 if ( ! response . ok ) {
7799 await response . body ?. cancel ( )
100+ if ( response . status === 401 || response . status === 403 ) {
101+ throw new TransportKeyRefused ( response . status )
102+ }
78103 throw new Error ( `Transport poll refused (HTTP ${ response . status } )` )
79104 }
105+ if ( outage ) {
106+ logger . info ( 'Sim transport reconnected' )
107+ outage = undefined
108+ }
80109 phase = 'parse'
81110 const batch = SimChannelBatchEnvelope . parse ( await response . json ( ) )
82111 const controls = batch . requests . flatMap ( ( raw ) => receiveControl ( raw ) ?? [ ] )
@@ -103,11 +132,28 @@ export async function receiveSimControls(
103132 if ( failed ?. status === 'rejected' ) throw failed . reason
104133 } catch ( error ) {
105134 if ( signal . aborted ) return
106- logger . warn ( 'Sim transport disconnected; reconnecting' , {
107- phase,
108- error : getErrorMessage ( error ) ,
109- } )
110- await sleep ( 1000 )
135+ const keyRefused = error instanceof TransportKeyRefused
136+ const kind = keyRefused ? 'key_refused' : 'disconnected'
137+ // One line per outage, and again only when it changes kind, not one per retry.
138+ if ( outage !== kind ) {
139+ outage = kind
140+ failures = 0
141+ if ( keyRefused ) {
142+ logger . warn ( 'Sim transport key refused by the worker; check COPILOT_API_KEY' , {
143+ status : error . status ,
144+ } )
145+ } else {
146+ logger . warn ( 'Sim transport disconnected; reconnecting' , {
147+ phase,
148+ error : getErrorMessage ( error ) ,
149+ } )
150+ }
151+ }
152+ failures ++
153+ await interruptibleSleep (
154+ backoffWithJitter ( failures , null , keyRefused ? REFUSED_BACKOFF : TRANSIENT_BACKOFF ) ,
155+ signal
156+ )
111157 }
112158 }
113159}
0 commit comments