v0.38.0

feder-cr/AIHawkv0.38.0Sep 10, 2026by github-actions[bot]

AI Summary

This release refactors the Server-Sent Events (SSE) implementation by centralizing framing logic into the `sse()` function and introducing `marker_at()`/`resume_point()` to prevent drift. The changes reduce code duplication and add a direct wire format test, ensuring no observable behavior changes for users.

Key Highlights

  • Centralized SSE framing logic to eliminate seven copies of hand-written code.
  • Introduced `marker_at()` and `resume_point()` to manage stream state and prevent drift.
  • Added `test_the_event_stream_wire_format.py` to verify byte-level wire format directly.
  • Reduced code complexity by simplifying `events()`, `stream()`, and `json.dumps` calls.
  • Fixed a reconnection bug where markers without positions were incorrectly handled.

New Features

  • Added `marker_at()` function to write resume points
  • Added `resume_point()` function to read resume points

Full Release Notes

Every event on `/chat/events` was assembled by hand: the optional `id:` line, the newline between it and the `data:` line, the json, the blank line that ends the event. Seven copies, five of them differing only in which dict goes in. Those three details are what a reader skims past and every one of them is load-bearing, so seven copies were seven chances to get one wrong somewhere nothing looked.

Now `sse()` knows the framing and nothing else does. `marker_at()` writes a resume point and `resume_point()` reads one, side by side so they cannot drift. The two empty replies that `/live/browsers` and `/live/tabs` give when there is nothing to say are named once instead of written out five times: they have to carry the same fields as the full replies, because the page reads the same fields either way, and they are returned exactly on the paths where something is missing, which is where a shape copied a second time goes stale unwatched.

`events()` drops from 42 lines of code to 31 and `stream()` from 27 to 21. `json.dumps` goes from seven calls to one.

**Nothing observable changes, and that is a measurement rather than a claim.** Every other test of the stream splits on `data: ` and decodes the json, so it sees the content and never the framing. `test_the_event_stream_wire_format.py` pins the bytes instead, and it was written and made green before any of this moved. It drives the application directly rather than through a test client, for two reasons: a client reassembles the response, which is the very thing under test, and it runs the app in another thread's event loop where the conversation the test built does not live.

It is a gate and not a green light: ten known-bad versions of the code, ten killed. Eight of those were tried against the old code first, and four went unnoticed by every other test in the suite, which is the measure of what the file adds. The tenth found a hole in the test rather than the code: a reconnection carrying a marker with no position in it, which is what a page held open across the upgrade that introduced the epoch sends, is correctly told to start over, and nothing had ever checked it.

The first commit is separate and is only line endings. The split that created `routes.py`, `chat.py` and `sessions.py` wrote them with an extra carriage return before each line ending, on 326, 211 and 165 lines. Python treats a lone CR as a line break too, so each of those lines counted twice and the modules parsed and ran exactly as intended, which is why nothing caught it. What it cost is stack traces: `Sessions.forget` sits on line 168 of a 180-line file and Python reported it at 281, past the end, so a traceback through these modules named a line that does not exist and printed no source at all. Counting CRLF does not reveal it, since the wrong sequence contains a correct one.

Measured while looking, and deliberately not changed here: `/live/tabs` starts a browser nobody asked for. On a fresh home with no instruction given, the process count goes 9, then 9 after `/live/browsers`, then 16 after `/live/tabs`, and the reply is `{"url": "", "tabs": []}`. The guard is `link.touched`, which is armed by the first call of any kind, and `browser_list` is precisely the question chosen because it starts nothing. The page already carries that rule and applies it only when a browser is named. Fixing it is a decision about what resolving an address may cause, not a mechanical change, so it is reported rather than folded into a refactor.

549 tests pass. Verified on the running interface as well: a real turn and three kinds of reconnection.