The SDK modelled 33 server-to-client events selected from roughly 60 the
backend emits, with nothing marking which of them carry a compatibility
guarantee. The supported scope names 19, and the two sets were not in a
containment relationship.
## Protocol surface
The modelled surface is now the supported scope and nothing else.
Four supported events were absent and are added, with models:
`session:llm_thinking`, `session:tool_status`, `session:required_action` and
`session:restriction`. Between them they carry what a task is doing and why it
stopped. The reasoning stream previously ran on `session:work_log`, which is
outside the scope and is a distinct event from `session:llm_thinking` rather
than a former name for it.
Events outside the scope are unmodelled but still delivered, unchanged and in
order. `is_supported_event()` distinguishes the two surfaces and
`emit_event()` sends an unmodelled event. This follows the scope's own
position: tolerating an unsupported event is required, depending on one is not.
## Requirements
Three requirements the scope states as MUST were unmet:
- **Recovery.** `session:join` now carries `since_revision` `"0"` and ignores
the incremental-synchronisation fields. `rebuild()` pages through history
until the cursor is exhausted, on every join and every reconnect. A short or
empty page does not indicate exhaustion.
- **Deduplication.** Events are keyed on the event identifier together with the
message type. Identifiers collide across types, so keying on the identifier
alone discards valid events.
- **Blocking conditions.** `InputState` exposes the reason from
`session:input_state`, including the two conditions the scope calls out.
Turn control no longer depends on unsupported events. It previously hinged on
`session:ask_for_location`, `session:interactive_auth_confirmation`,
`session:three_way_call` and `session:reward`.
## Tests
Three layers. `tests/protocol/fixtures` holds one envelope per supported event,
with provenance recorded — 14 captured from live sessions, 4 derived from the
protocol definition for conditions an ordinary session does not reach.
`test_contract.py` validates one envelope at a time. `test_flow.py` drives
sequences through the real transport and pins each MUST by name, including that
an unrecognised event arrives verbatim without disturbing ordering.
`tests/integration` was two overlapping copies of one live script, one of which
asserted behaviour the SDK had already dropped. It is now a single suite that
also serves as the fixture recorder. It places one call to a number in the
range NANP reserves for fiction, which is what makes `session:task_ready`,
`session:tool_status` and `session:task_finished` observable. It stays out of
CI: it requires a token and consumes credits.
CI gains a lint step. `ruff` was configured but never invoked.
## Fixed
Sessions joined through `join_session()` were never re-joined after a
reconnect. Membership was tracked on the fire-and-forget emit path, while
joining is a request/response call.
## Breaking
`send_auth_confirmation()`, `send_location_response()` and
`send_location_selection()` are removed, along with `NotificationEvent`, the
`session:reward` and `session:payment` models, the unsupported event constants,
the `action` argument on `chat()`, and `request_work_log` on `get_history()`.
Each remains reachable through `emit_event()`.
`pine-mcp-server` calls the three removed methods and depends on
`pine-assistant>=0.3.2`. It requires a corresponding change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>