( config: BridgeConfig, environmentId: string, environmentSecret: string, api: BridgeApiClient, spawner: SessionSpawner, logger: BridgeLogger, signal: AbortSignal, backoffConfig: BackoffConfig = DEFAULT_BACKOFF, initialSessionId?: string, getAccessToken?: () => string | undefined | Promise<string | undefined>, )
| 139 | } |
| 140 | |
| 141 | export async function runBridgeLoop( |
| 142 | config: BridgeConfig, |
| 143 | environmentId: string, |
| 144 | environmentSecret: string, |
| 145 | api: BridgeApiClient, |
| 146 | spawner: SessionSpawner, |
| 147 | logger: BridgeLogger, |
| 148 | signal: AbortSignal, |
| 149 | backoffConfig: BackoffConfig = DEFAULT_BACKOFF, |
| 150 | initialSessionId?: string, |
| 151 | getAccessToken?: () => string | undefined | Promise<string | undefined>, |
| 152 | ): Promise<void> { |
| 153 | // Local abort controller so that onSessionDone can stop the poll loop. |
| 154 | // Linked to the incoming signal so external aborts also work. |
| 155 | const controller = new AbortController() |
| 156 | if (signal.aborted) { |
| 157 | controller.abort() |
| 158 | } else { |
| 159 | signal.addEventListener('abort', () => controller.abort(), { once: true }) |
| 160 | } |
| 161 | const loopSignal = controller.signal |
| 162 | |
| 163 | const activeSessions = new Map<string, SessionHandle>() |
| 164 | const sessionStartTimes = new Map<string, number>() |
| 165 | const sessionWorkIds = new Map<string, string>() |
| 166 | // Compat-surface ID (session_*) computed once at spawn and cached so |
| 167 | // cleanup and status-update ticks use the same key regardless of whether |
| 168 | // the tengu_bridge_repl_v2_cse_shim_enabled gate flips mid-session. |
| 169 | const sessionCompatIds = new Map<string, string>() |
| 170 | // Session ingress JWTs for heartbeat auth, keyed by sessionId. |
| 171 | // Stored separately from handle.accessToken because the token refresh |
| 172 | // scheduler overwrites that field with the OAuth token (~3h55m in). |
| 173 | const sessionIngressTokens = new Map<string, string>() |
| 174 | const sessionTimers = new Map<string, ReturnType<typeof setTimeout>>() |
| 175 | const completedWorkIds = new Set<string>() |
| 176 | const sessionWorktrees = new Map< |
| 177 | string, |
| 178 | { |
| 179 | worktreePath: string |
| 180 | worktreeBranch?: string |
| 181 | gitRoot?: string |
| 182 | hookBased?: boolean |
| 183 | } |
| 184 | >() |
| 185 | // Track sessions killed by the timeout watchdog so onSessionDone can |
| 186 | // distinguish them from server-initiated or shutdown interrupts. |
| 187 | const timedOutSessions = new Set<string>() |
| 188 | // Sessions that already have a title (server-set or bridge-derived) so |
| 189 | // onFirstUserMessage doesn't clobber a user-assigned --name / web rename. |
| 190 | // Keyed by compatSessionId to match logger.setSessionTitle's key. |
| 191 | const titledSessions = new Set<string>() |
| 192 | // Signal to wake the at-capacity sleep early when a session completes, |
| 193 | // so the bridge can immediately accept new work. |
| 194 | const capacityWake = createCapacityWake(loopSignal) |
| 195 | |
| 196 | /** |
| 197 | * Heartbeat all active work items. |
| 198 | * Returns 'ok' if at least one heartbeat succeeded, 'auth_failed' if any |
no test coverage detected