(newEvents: SDKMessage[])
| 99 | } |
| 100 | |
| 101 | ingest(newEvents: SDKMessage[]): ScanResult { |
| 102 | for (const m of newEvents) { |
| 103 | if (m.type === 'assistant') { |
| 104 | for (const block of m.message.content) { |
| 105 | if (block.type !== 'tool_use') continue |
| 106 | const tu = block as ToolUseBlock |
| 107 | if (tu.name === EXIT_PLAN_MODE_V2_TOOL_NAME) { |
| 108 | this.exitPlanCalls.push(tu.id) |
| 109 | } |
| 110 | } |
| 111 | } else if (m.type === 'user') { |
| 112 | const content = m.message.content |
| 113 | if (!Array.isArray(content)) continue |
| 114 | for (const block of content) { |
| 115 | if (block.type === 'tool_result') { |
| 116 | this.results.set(block.tool_use_id, block) |
| 117 | } |
| 118 | } |
| 119 | } else if (m.type === 'result' && m.subtype !== 'success') { |
| 120 | // result(success) fires after EVERY CCR turn |
| 121 | // If the remote asks a clarifying question (turn ends without |
| 122 | // ExitPlanMode), we must keep polling — the user can reply in |
| 123 | // the browser and reach ExitPlanMode in a later turn. |
| 124 | // Only error subtypes (error_during_execution, error_max_turns, |
| 125 | // etc.) mean the session is actually dead. |
| 126 | this.terminated = { subtype: m.subtype } |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | // Skip-scan when nothing could have moved the target: no new events, no |
| 131 | // rejection last tick. A rejection moves the newest-non-rejected target. |
| 132 | const shouldScan = newEvents.length > 0 || this.rescanAfterRejection |
| 133 | this.rescanAfterRejection = false |
| 134 | |
| 135 | let found: |
| 136 | | { kind: 'approved'; plan: string } |
| 137 | | { kind: 'teleport'; plan: string } |
| 138 | | { kind: 'rejected'; id: string } |
| 139 | | { kind: 'pending' } |
| 140 | | null = null |
| 141 | if (shouldScan) { |
| 142 | for (let i = this.exitPlanCalls.length - 1; i >= 0; i--) { |
| 143 | const id = this.exitPlanCalls[i]! |
| 144 | if (this.rejectedIds.has(id)) continue |
| 145 | const tr = this.results.get(id) |
| 146 | if (!tr) { |
| 147 | found = { kind: 'pending' } |
| 148 | } else if (tr.is_error === true) { |
| 149 | const teleportPlan = extractTeleportPlan(tr.content) |
| 150 | found = |
| 151 | teleportPlan !== null |
| 152 | ? { kind: 'teleport', plan: teleportPlan } |
| 153 | : { kind: 'rejected', id } |
| 154 | } else { |
| 155 | found = { kind: 'approved', plan: extractApprovedPlan(tr.content) } |
| 156 | } |
| 157 | break |
| 158 | } |
no test coverage detected