github feder-cr/AIHawk v0.68.8
0.68.8

6 hours ago

Deleting the default conversation from the sessions panel killed the whole
interface. Measured over plain HTTP, no browser, no page:

/sessions/forget on any other conversation -> 200, interface alive
/sessions/forget on the DEFAULT            -> 200, process DEAD, exit 1

Link.open called __aenter__ on stdio_client and on ClientSession by
hand, and Link.close called __aexit__ on them - from whatever task happened
to be closing. Both are anyio context managers, so each owns a cancel scope,
and a cancel scope has to be exited in the task that ENTERED it. Exiting it
elsewhere delivers the cancellation to the scope enclosing the entering task.

cli.serve opens the default conversation and then runs uvicorn in the same
task, so closing that link from a request task cancelled server.serve().

And the others were not fine, they were quiet. Their scopes belong to request
tasks that had already finished, so the same wrong exit raised
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in, which close was swallowing under a sentence written about
teardown failures. EVERY close was wrong; exactly one of them had something
alive to damage. Making only the default lazy would have removed the visible
half and kept the defect.

So the connection has an OWNER. One task opens it, publishes it, waits to be
told to stop, and closes it itself; open starts that task and waits until the
connection is usable, close asks it to let go and waits until it has. Closing
from another task is not guarded against - it is made impossible, because no
other task ever holds the contexts. _ctx and _sess_ctx are gone with it.

A failure BEFORE the connection is usable is now open's to report rather than
to swallow: it is the difference between being told the server did not start
and a page waiting for one that never will.

Six tests against the real server over stdio, because the defect is about task
ownership of a real transport and a double cannot have it. Five known-bad
inputs, all killed: the whole previous implementation put back (three of the
six go red with the cancel-scope sentence verbatim), an owner that does not
wait, an open that cannot report a failure, a close that does not wait for the
owner, and a close that does not even ask it to stop.

Two of those five SURVIVED the first draft of the tests, and both survivals
were real holes rather than bad mutations. Nothing asserted that an open
connection ANSWERS, so a link that closed itself the instant after it was
published read exactly like a live one; and the failed-open arm accepted any
exception, including the TimeoutError that means it hung, which is the very
thing it exists to forbid. Both are fixed in the tests, not in the code.

mcp/session.py has the same SHAPE on InvisiblePlaywright - __aenter__ in
start, __aexit__ in close - and was checked rather than assumed: no file in
invisible_playwright imports anyio, so there is no cancel scope there and no
such failure to have. Written down because an audit that names only the broken
places does not say how much it looked at.

Verified on the real product: deleting the default conversation now leaves the
interface alive and /sessions answering 200.

Suite: 738 passed, 9 skipped. Lint, invisible_core.english and
check_content.py clean.

Don't miss a new AIHawk release

NewReleases is sending notifications on new releases.