( sessionId: string, timeoutMs: number, onPhaseChange?: (phase: UltraplanPhase) => void, shouldStop?: () => boolean, )
| 196 | // text embedded in the feedback. Normal rejections (is_error === true, no |
| 197 | // sentinel) are tracked and skipped so the user can iterate in the browser. |
| 198 | export async function pollForApprovedExitPlanMode( |
| 199 | sessionId: string, |
| 200 | timeoutMs: number, |
| 201 | onPhaseChange?: (phase: UltraplanPhase) => void, |
| 202 | shouldStop?: () => boolean, |
| 203 | ): Promise<PollResult> { |
| 204 | const deadline = Date.now() + timeoutMs |
| 205 | const scanner = new ExitPlanModeScanner() |
| 206 | let cursor: string | null = null |
| 207 | let failures = 0 |
| 208 | let lastPhase: UltraplanPhase = 'running' |
| 209 | |
| 210 | while (Date.now() < deadline) { |
| 211 | if (shouldStop?.()) { |
| 212 | throw new UltraplanPollError( |
| 213 | 'poll stopped by caller', |
| 214 | 'stopped', |
| 215 | scanner.rejectCount, |
| 216 | ) |
| 217 | } |
| 218 | let newEvents: SDKMessage[] |
| 219 | let sessionStatus: PollRemoteSessionResponse['sessionStatus'] |
| 220 | try { |
| 221 | // Metadata fetch (session_status) is the needs_input signal — |
| 222 | // threadstore doesn't persist result(success) turn-end events, so |
| 223 | // idle status is the only authoritative "remote is waiting" marker. |
| 224 | const resp = await pollRemoteSessionEvents(sessionId, cursor) |
| 225 | newEvents = resp.newEvents |
| 226 | cursor = resp.lastEventId |
| 227 | sessionStatus = resp.sessionStatus |
| 228 | failures = 0 |
| 229 | } catch (e) { |
| 230 | const transient = isTransientNetworkError(e) |
| 231 | if (!transient || ++failures >= MAX_CONSECUTIVE_FAILURES) { |
| 232 | throw new UltraplanPollError( |
| 233 | e instanceof Error ? e.message : String(e), |
| 234 | 'network_or_unknown', |
| 235 | scanner.rejectCount, |
| 236 | { cause: e }, |
| 237 | ) |
| 238 | } |
| 239 | await sleep(POLL_INTERVAL_MS) |
| 240 | continue |
| 241 | } |
| 242 | |
| 243 | let result: ScanResult |
| 244 | try { |
| 245 | result = scanner.ingest(newEvents) |
| 246 | } catch (e) { |
| 247 | throw new UltraplanPollError( |
| 248 | e instanceof Error ? e.message : String(e), |
| 249 | 'extract_marker_missing', |
| 250 | scanner.rejectCount, |
| 251 | ) |
| 252 | } |
| 253 | if (result.kind === 'approved') { |
| 254 | return { |
| 255 | plan: result.plan, |
no test coverage detected