From 9b3c749302e883fc603c9fbe0bbc8e7639f9eef8 Mon Sep 17 00:00:00 2001 From: laitekin <2625707287@qq.com> Date: Mon, 21 Sep 2026 23:03:24 +0800 Subject: [PATCH] docs(run-journal): specify live-only metering and optional writes (#7272) --- api/run_journal.py | 2 +- docs/rfcs/session-sse-contract-v1.md | 27 ++++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/api/run_journal.py b/api/run_journal.py index 677876eb..a088c13d 100644 --- a/api/run_journal.py +++ b/api/run_journal.py @@ -454,7 +454,7 @@ class RunJournalWriter: self._path = _run_path(self.session_id, self.run_id, session_dir=self.session_dir) self._lock = _lock_for(self._path) - def append_sse_event(self, event_name: str, payload=None) -> dict: + def append_sse_event(self, event_name: str, payload=None) -> dict | None: # Live-UI-only telemetry (metering) has no recovery value in the journal: # nothing reads those rows back for recovery, and journaling them at ~10 Hz # on marathon runs balloons the durable file (12+ MB of a single 18 MB run diff --git a/docs/rfcs/session-sse-contract-v1.md b/docs/rfcs/session-sse-contract-v1.md index 65b13366..e6248e03 100644 --- a/docs/rfcs/session-sse-contract-v1.md +++ b/docs/rfcs/session-sse-contract-v1.md @@ -183,7 +183,7 @@ when integrating with the live chat SSE relay. | `cancel` | Run cancelled | | `done` | Turn finalized (session payload); title/`stream_end` may follow | | `stream_end` | SSE fence — close the client EventSource | -| `metering` | Token/cost metering snapshot | +| `metering` | Best-effort live token/cost metering snapshot; not journal-replayed | | `context_status` | Context window / usage status | | `goal` | Goal / plan card update | | `goal_continue` | Goal continuation signal | @@ -228,6 +228,31 @@ A future implementation must replay from the run journal via the existing `_replay_run_journal()` path (in `api/routes.py`) and fall back to the snapshot mechanism when journal entries are unavailable for a given cursor. +### Live-only metering (implemented replay behavior) + +`metering` is best-effort live telemetry, not durable recovery content. +`RunJournalWriter.append_sse_event()` returns `None` for metering without +writing a row or allocating a sequence number. Other journaled events retain +contiguous sequence numbers. Local-agent and gateway producers carry an +explicit per-frame event ID, including `None`, through `StreamChannel`; +an unjournaled metering frame emits no new SSE `id:` and must not borrow the +previous frame's journal ID. The last-ID fallback is limited to legacy +two-element queue entries. + +Existing journals are not rewritten. Direct readers (`read_run_events()` and +`read_session_run_events()`) retain legacy metering rows for cursor validation +and gap/coverage checks. Both run-level and per-session journal replay emitters +apply `journal_replay_visible()` to omit those rows from reconnect delivery. +Consequently, replayed legacy event IDs can have gaps where metering was +filtered; clients must not infer missing durable content from those gaps. +Metering missed while disconnected is not recovered from the journal; fresh +live updates remain available. This exception does not remove token, reasoning, +tool, or terminal events from durable replay. + +Coverage: `tests/test_run_journal.py`, `tests/test_run_journal_routes.py`, +`tests/test_issue4812_session_sse_stream.py`, and +`tests/test_stage364_opus_live_sse_event_id.py`. + ## Snapshot fallback When the `Last-Event-ID` cursor is evicted, expired, unknown, or refers to a