The session-alert lane: the events worth waking an operator for, and their
one-line relay.
Plan 237 (CAPS-01), built to the shape plan 231 established for
``worker-reports``; plan 248 (CAPS-03) added the fourth kind. A running OC Chat
session that crosses a cost checkpoint, stops at the dollar hard cap, pauses at
the per-turn tool-call checkpoint, or halts a turn on a detected stall
writes ONE event into ``comms/lanes/session-alerts/`` and stops there: the
product holds no credential, knows nothing about connectors, and touches no
other lane. This module is the other half โ the orchestrator-side reader and
the grant-checked, egress-filtered relay that puts the alert on the operator's
phone.
**This module is the schema authority for the lane.** The writer lives in
``oc_chat/oc_chat/core/session_alerts.py`` and deliberately does NOT import
this package โ that is what keeps the product credential-free. The two sides
agree by written contract rather than by a shared import, and the contract is
:class:`SessionAlertEvent`::
{
"schema_version": "1.0",
"event_id": "<26-character ULID, time-first so lexical == arrival>",
"lane": "session-alerts",
"direction": "inbound",
"risk_class": "low",
"created_at_utc": "2026-09-06T12:00:00Z",
"session_id": "<the chat session's stable uuid4>",
"kind": "cost_threshold" | "cost_hard_cap"
| "tool_call_checkpoint" | "stall_halt",
"threshold_usd": 2.0, // null for a tool-call checkpoint
// and for a stall halt
"total_usd": 2.14,
"unpriced_samples": 3,
"turn_tool_calls": 100,
"summary": "<the one line the operator reads, numbers only>",
"relayed_at_utc": null,
"relay_chunk_count": null
}
File naming follows the ยง3 bus convention, ``<utc-stamp>_product_<ulid>.json``.
Two differences from the worker-report lane are deliberate:
* **One line, never chunked.** A worker report carries the worker's whole
closing summary and must be chunked to survive Telegram's 4096-character
limit. An alert is a single generated sentence of numbers, so the relay sends
exactly one message per event and records ``relay_chunk_count = 1``. If a
summary ever grew past the transport limit the send would fail and the event
would stay unrelayed โ a loud failure, not a truncation.
* **No content digest.** A worker report's ``summary_sha256`` exists because the
text is a human's words and a post-hoc edit would put words in their mouth.
An alert's summary is derived from the numeric fields beside it, so the
relay re-derives nothing and asserts nothing about authorship; what it
forwards is the stored line, fenced, exactly as it forwards a worker's.
Dependencies
------------
- oc_connect.audit (AuditStore, content_hash, timestamp, utc_now - audits relay outcomes and stamps relayed_at_utc)
- oc_connect.delivery (OutboundSender - the grant-checked send path every relayed alert crosses)
- oc_connect.egress (require_safe, EgressBlockedError - scans the alert summary before it is queued)
- oc_diagnostics (_dbg structured logger)
- option_c_connector_contracts (lane - the session-alerts lane's declared direction and risk ceiling)
- pydantic (BaseModel, ConfigDict, field_validator - the SessionAlertEvent schema)