Shared decoder for the `decision_resolved` leading frame emitted by the `confirm-tool` SSE route. Parses the first NUL-delimited JSON chunk from the stream, drains remaining bytes to avoid half-closed socket hangs, and returns a typed `DecisionResolvedFrame`. ## Key Components ### `DecisionResolvedFrame` (interface) Typed shape of the leading SSE frame returned by `confirm-tool/route.ts`. Key fields: | Field | Type | Description | |---|---|---| | `kind` | `'decision_resolved'` | Frame discriminator | | `ok` | `boolean` | Whether the operation succeeded | | `action` | `'approved' \| 'rejected'` | Tool confirmation outcome | | `willAutoContinue` | `boolean?` | Auto-continue flag post-approval | | `result` | `object?` | Approve-path envelope; `mirror_synced: false` means HubSpot succeeded but local mirror lagged — callers should optimistic-render and schedule a delayed refetch | | `card` | `object?` | UI card metadata | | `receiptText` | `string?` | Human-readable receipt | ### `readLeadingDecisionFrame(response)` (async function) Reads and decodes the first `decision_resolved` frame from a `confirm-tool` SSE `Response`. Drains the full stream before resolving. Throws if the body is empty, the first frame is not `decision_resolved`, or the leading JSON is malformed. ### `normalizeDecisionFrame(obj)` (internal) Safely casts a raw parsed object into a `DecisionResolvedFrame`, applying defaults and conditional spreads for optional fields. ## Usage Example ```typescript const response = await fetch('/api/confirm-tool', { method: 'POST', body: JSON.stringify({ proposalId: 'prop_123' }), }) const frame = await readLeadingDecisionFrame(response) if (!frame.ok || frame.action === 'rejected') { console.warn('Tool rejected:', frame.receiptText) return } if (frame.result?.mirror_synced === false) { // HubSpot succeeded but mirror lagged — optimistic render + refetch scheduleDelayedRefetch(frame.result.ticket_id) } ``` > **Protocol note:** The tickets-UI path sends `messages: []`, making the stream a single `decision_resolved` frame followed by EOF. The `\x1E` record-separator sentinel (phase-2 text body) is structurally unreachable in this path — its presence before `\0` is treated as a protocol violation and throws.