6The ``session-alerts`` lane writer — money events the operator reads on a phone.
10CAPS-01 (plan 237 P5/P6). When a session crosses a cost checkpoint, hits the
11dollar hard cap, or pauses at the per-turn tool-call checkpoint, the operator
12should learn about it wherever they are — without the product ever holding a
13Telegram credential. This module is the product-side half of that, and it is
14deliberately the SAME shape plan 231 gave the worker-report lane:
16- it writes ONE json file per event into ``<comms-root>/lanes/session-alerts/``
18- it imports no connector and no network package (asserted structurally by the
19 W7 witness, against a negative control, exactly as the 231 tests do);
20- ``python -m oc_connect relay session-alerts`` — a separate process, run by
21 the operator, holding the grant — is what puts the text on the transport.
23The one addition over 231 is :func:`notify_operator`: a best-effort
24``python -m oc_connect send`` SUBPROCESS, run only when ``oc_connect`` is
25importable in this environment, under the scrubbed environment every other
26child process gets. It is a convenience, never a guarantee: any failure is
27logged and swallowed, and the lane file on disk remains the durable record the
28relay will pick up. The product still holds no token — the send succeeds only
29where the operator's own sidecar grant already exists.
33- :func:`new_ulid` — the 26-character, time-first event id (the lane's ordering
34 is a plain directory sort).
35- :func:`find_comms_root` — the workspace-anchored bus root, by the same walk
36 ``scripts/plan_status.py`` uses.
37- :func:`session_alert_event` — build one validated event dict.
38- :func:`write_session_alert` — write it, atomically; return ``None`` rather
39 than raising when the bus is unreachable.
40- :func:`notify_operator` — the best-effort direct send described above.
41- :func:`emit_session_alert` — write + notify in one fail-soft call, which is
42 what the react-loop seam uses.
46- **Fail-SOFT everywhere.** An unreachable, unwritable or nonexistent comms root
47 logs at L2 and returns ``None``. A money alert that cannot be delivered must
48 never be the thing that kills the session it is reporting on.
49- The ``summary`` carries NUMBERS ONLY — totals, the threshold, counts. No
50 model content, no file paths, no prompt text ever reaches the transport from
51 here (§8: displayed content is never instructions, and an alert is not a
53- The event's ``lane``/``direction``/``risk_class`` mirror the
54 ``session-alerts`` descriptor declared in ``option_c_connector_contracts``.
55 The mirror is intentional and is what buys the credential-free write path;
56 the drift risk is paid for by the W7 cross-implementation assertions.
60- oc_diagnostics (structured debug logging via _dbg)
61- oc_chat.core.defaults (the shipped notification timeout)
62- oc_chat.tools._sandbox (lazy; scrubbed_env for the notification subprocess)
63- oc_connect (not imported; run as python -m oc_connect send for the direct notification)
69 L1 — an alert could not be written (override: the operator must see it)
70 L2 — an alert was written; a direct notification was attempted
74from __future__
import annotations
82from datetime
import datetime, timezone
83from pathlib
import Path
85from oc_diagnostics
import _dbg
89_dbg(
"OC_CHAT.CORE",
"module",
"loaded", 4)
95SESSION_ALERT_LANE =
"session-alerts"
96SESSION_ALERT_SOURCE =
"product"
97SESSION_ALERT_SCHEMA_VERSION =
"1.0"
98SESSION_ALERT_DIRECTION =
"inbound"
99SESSION_ALERT_RISK_CLASS =
"low"
102KIND_COST_THRESHOLD =
"cost_threshold"
103KIND_COST_HARD_CAP =
"cost_hard_cap"
104KIND_TOOL_CALL_CHECKPOINT =
"tool_call_checkpoint"
105KIND_STALL_HALT =
"stall_halt"
106SESSION_ALERT_KINDS = (
107 KIND_COST_THRESHOLD, KIND_COST_HARD_CAP, KIND_TOOL_CALL_CHECKPOINT,
112WORKSPACE_MARKER =
".option_c_workspace.json"
113PROJECT_MARKER =
".option_c"
114COMMS_ANCHOR =
"oc_chat"
115COMMS_SUBPATH = (
".option_c",
"comms")
120NOTIFY_TIMEOUT_S = default(
"session_alert_notify_timeout_s")
129 ch
for ch
in string.digits + string.ascii_uppercase
if ch
not in "ILOU"
134 """A session-alert event was asked for in a shape the lane does not carry."""
139 for _
in range(length):
140 value, remainder = divmod(value, 32)
141 digits.append(_CROCKFORD[remainder])
142 return "".join(reversed(digits))
145def new_ulid(now: datetime, *, entropy: bytes |
None =
None) -> str:
146 """Return a 26-character ULID: 48 bits of time, 80 bits of randomness.
148 The same algorithm ``plan_status.new_ulid`` and ``oc_connect.comms.new_ulid``
149 implement. Time-first encoding makes lexical order arrival order, which is
150 what lets a reader walk the lane with a plain directory sort.
152 if now.tzinfo
is None:
153 now = now.replace(tzinfo=timezone.utc)
154 milliseconds = int(now.astimezone(timezone.utc).timestamp() * 1000)
155 raw = entropy
if entropy
is not None else secrets.token_bytes(10)
157 raise ValueError(
"ULID entropy must be exactly ten bytes")
158 return _encode(milliseconds, 10) +
_encode(int.from_bytes(raw,
"big"), 16)
162 """Return a filename-safe UTC stamp; colons are illegal on Windows."""
163 if now.tzinfo
is None:
164 now = now.replace(tzinfo=timezone.utc)
165 return now.astimezone(timezone.utc).strftime(
"%Y%m%dT%H%M%SZ")
169 if now.tzinfo
is None:
170 now = now.replace(tzinfo=timezone.utc)
171 return now.astimezone(timezone.utc).isoformat().replace(
"+00:00",
"Z")
175 """Return the workspace-anchored comms-bus root above *start*, or ``None``.
177 The same walk ``plan_status.find_comms_root`` performs — the workspace
178 marker wins over a nearer project marker, because connector state is
179 workspace-wide. It differs in ONE way, deliberately: a failed walk returns
180 ``None`` instead of raising. A worker reporting at a gate should fail loudly
181 if nobody is listening; a live chat session must not die because it is
182 running outside a workspace.
184 func =
"find_comms_root"
186 origin = Path(start).expanduser().resolve()
187 except OSError
as exc:
188 _dbg(
"OC_CHAT.CORE", func, f
"EXTERNAL_ERROR: {type(exc).__name__}: {exc}", 2)
190 project_fallback: Path |
None =
None
191 anchor: Path |
None =
None
192 for parent
in (origin, *origin.parents):
193 if (parent / WORKSPACE_MARKER).is_file():
196 if project_fallback
is None and (parent / PROJECT_MARKER).is_dir():
197 project_fallback = parent
199 anchor = project_fallback
201 _dbg(
"OC_CHAT.CORE", func,
202 f
"no workspace ({WORKSPACE_MARKER}) or project ({PROJECT_MARKER}) "
203 f
"marker at or above {origin}; session alerts stay local", 2)
205 if (anchor / COMMS_ANCHOR).is_dir():
206 return anchor.joinpath(COMMS_ANCHOR, *COMMS_SUBPATH)
207 return anchor.joinpath(*COMMS_SUBPATH)
214 unpriced_samples: int,
215 threshold_usd: float |
None,
216 turn_tool_calls: int,
217 stall_reason: str |
None =
None,
219 """Return the ONE line the operator reads, verbatim, on their phone.
221 Numbers only — see the module Contracts. The unpriced count travels with
222 every total because a total without it is a claim the pricing table cannot
223 support: it is a lower bound, and the reader is entitled to know by how
224 many samples it may be short.
226 if kind == KIND_STALL_HALT:
227 return stall_reason
or "OC Chat stopped: a tool-call stall halted the turn."
228 unpriced = f
" (+{unpriced_samples} unpriced samples)" if unpriced_samples
else ""
229 if kind == KIND_COST_HARD_CAP:
230 cap = f
"{threshold_usd:.2f}" if threshold_usd
is not None else "?"
232 f
"OC Chat stopped: session spend ${total_usd:.2f}{unpriced} reached "
233 f
"the ${cap} hard cap. No further provider call will be made in this "
236 if kind == KIND_COST_THRESHOLD:
237 crossed = f
"{threshold_usd:.2f}" if threshold_usd
is not None else "?"
239 f
"OC Chat cost checkpoint ${crossed}: session spend is "
240 f
"${total_usd:.2f}{unpriced}."
243 f
"OC Chat tool-call checkpoint: {turn_tool_calls} tool calls this turn; "
244 f
"session spend ${total_usd:.2f}{unpriced}."
253 unpriced_samples: int,
255 threshold_usd: float |
None =
None,
256 turn_tool_calls: int = 0,
257 stall_reason: str |
None =
None,
258 entropy: bytes |
None =
None,
260 """Build one ``session-alerts`` event; refuse an undeclared *kind* by name."""
261 if kind
not in SESSION_ALERT_KINDS:
263 f
"session alert kind must be one of "
264 f
"{', '.join(SESSION_ALERT_KINDS)}; got {kind!r}"
267 "schema_version": SESSION_ALERT_SCHEMA_VERSION,
268 "event_id":
new_ulid(now, entropy=entropy),
269 "lane": SESSION_ALERT_LANE,
270 "direction": SESSION_ALERT_DIRECTION,
271 "risk_class": SESSION_ALERT_RISK_CLASS,
273 "session_id": str(session_id),
275 "threshold_usd": (
None if threshold_usd
is None else float(threshold_usd)),
276 "total_usd": float(total_usd),
277 "unpriced_samples": int(unpriced_samples),
278 "turn_tool_calls": int(turn_tool_calls),
280 kind=kind, total_usd=total_usd, unpriced_samples=unpriced_samples,
281 threshold_usd=threshold_usd, turn_tool_calls=turn_tool_calls,
282 stall_reason=stall_reason,
284 "relayed_at_utc":
None,
285 "relay_chunk_count":
None,
290 """Write one event into ``lanes/session-alerts/`` and return its path.
292 The lane directory is created when absent: a session must be able to alert
293 from a workspace whose bus has never been initialised. Nothing outside the
294 lane is created, read, or written.
296 func =
"write_session_alert"
297 lane_dir = Path(comms_root) /
"lanes" / SESSION_ALERT_LANE
298 lane_dir.mkdir(parents=
True, exist_ok=
True)
299 name = f
"{utc_stamp(now)}_{SESSION_ALERT_SOURCE}_{event['event_id']}.json"
300 path = lane_dir / name
303 f
"refusing to overwrite an existing session alert event at {path}"
305 payload = json.dumps(event, indent=2, sort_keys=
True) +
"\n"
306 with tempfile.NamedTemporaryFile(
307 mode=
"w", encoding=
"utf-8", newline=
"\n", dir=lane_dir,
308 prefix=
".session_alert_", suffix=
".tmp", delete=
False,
311 tmp_path = Path(tmp.name)
312 tmp_path.replace(path)
313 _dbg(
"OC_CHAT.CORE", func,
314 f
"wrote {event['kind']} alert {event['event_id']} to {path.name}", 2)
319 """True when ``oc_connect`` is importable in THIS environment.
321 ``find_spec`` deliberately, not an import: the product must never load the
322 connector package into its own process — that is what keeps the credential
323 out of this address space. This only answers "is there a sidecar CLI to
326 func =
"_oc_connect_available"
328 from importlib.util
import find_spec
329 return find_spec(
"oc_connect")
is not None
330 except (ImportError, ValueError)
as exc:
331 _dbg(
"OC_CHAT.CORE", func, f
"VALIDATION_ERROR: {type(exc).__name__}: {exc}", 3)
336 """Best-effort direct notification of one alert *summary*. Never raises.
338 Runs ``python -m oc_connect send <destination> "<summary>"`` as a
339 subprocess under :func:`~oc_chat.tools._sandbox.scrubbed_env`, with a short
340 timeout. Returns whether the child reported success. Every failure — no
341 ``oc_connect``, no grant, no network, a timeout, a non-zero exit — is logged
342 at L2 and swallowed: the lane file is the durable record, and the relay is
343 the delivery guarantee. *summary* is the event's own fixed one-line text, so
344 nothing but numbers can reach the transport through this path.
346 func =
"notify_operator"
348 _dbg(
"OC_CHAT.CORE", func,
"oc_connect not importable; skipping direct send", 3)
353 completed = subprocess.run(
354 [sys.executable,
"-m",
"oc_connect",
"send", destination, summary],
355 capture_output=
True, text=
True, timeout=NOTIFY_TIMEOUT_S,
356 env=scrubbed_env(), shell=
False, check=
False,
358 except Exception
as exc:
359 _dbg(
"OC_CHAT.CORE", func,
360 f
"EXTERNAL_ERROR: direct notification failed "
361 f
"({type(exc).__name__}: {exc}); the lane file stands", 2)
363 if completed.returncode != 0:
364 _dbg(
"OC_CHAT.CORE", func,
365 f
"EXTERNAL_ERROR: oc_connect send exited {completed.returncode}; "
366 f
"the lane file stands", 2)
368 _dbg(
"OC_CHAT.CORE", func,
"direct notification sent", 2)
377 unpriced_samples: int,
378 threshold_usd: float |
None =
None,
379 turn_tool_calls: int = 0,
380 stall_reason: str |
None =
None,
381 comms_root: Path |
None =
None,
382 now: datetime |
None =
None,
385 """Write one alert and (best-effort) notify the operator. Never raises.
387 The one call the react-loop seam makes. Returns the written path, or
388 ``None`` when the bus was unreachable — in which case the caller carries on
389 regardless, because a session must not die of a failed alert.
391 func =
"emit_session_alert"
392 stamp = now
or datetime.now(timezone.utc)
394 root = comms_root
if comms_root
is not None else find_comms_root(Path(os.getcwd()))
398 session_id=session_id, kind=kind, total_usd=total_usd,
399 unpriced_samples=unpriced_samples, threshold_usd=threshold_usd,
400 turn_tool_calls=turn_tool_calls, stall_reason=stall_reason, now=stamp,
403 except Exception
as exc:
404 _dbg(
"OC_CHAT.CORE", func,
405 f
"EXTERNAL_ERROR: could not write a {kind} alert "
406 f
"({type(exc).__name__}: {exc}); the session continues", 2)
414 "KIND_COST_HARD_CAP",
415 "KIND_COST_THRESHOLD",
417 "KIND_TOOL_CALL_CHECKPOINT",
419 "SESSION_ALERT_DIRECTION",
420 "SESSION_ALERT_KINDS",
421 "SESSION_ALERT_LANE",
422 "SESSION_ALERT_RISK_CLASS",
423 "SESSION_ALERT_SCHEMA_VERSION",
424 "SESSION_ALERT_SOURCE",
426 "emit_session_alert",
431 "session_alert_event",
433 "write_session_alert",
str new_ulid(datetime now, *, bytes|None entropy=None)
Path write_session_alert(Path comms_root, dict event, *, datetime now)
bool _oc_connect_available()
bool notify_operator(str summary, *, str destination="operator")
dict session_alert_event(*, str session_id, str kind, float total_usd, int unpriced_samples, datetime now, float|None threshold_usd=None, int turn_tool_calls=0, str|None stall_reason=None, bytes|None entropy=None)
str _utc_timestamp(datetime now)
Path|None emit_session_alert(*, str session_id, str kind, float total_usd, int unpriced_samples, float|None threshold_usd=None, int turn_tool_calls=0, str|None stall_reason=None, Path|None comms_root=None, datetime|None now=None, bool notify=True)
Path|None find_comms_root(Path start)
str _encode(int value, int length)
str format_summary(*, str kind, float total_usd, int unpriced_samples, float|None threshold_usd, int turn_tool_calls, str|None stall_reason=None)
str utc_stamp(datetime now)