<!-- source-hash: ca8aad98b81a1dbccc525ac143c34ff1 -->
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.