From ac54e5e715a2c85d8e35964d811a96bf2ff29c63 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sat, 12 Sep 2026 08:17:46 -0700 Subject: [PATCH] fix(api): forked sessions carry _branched_from so they stay listable Review finding: with the child now created before the parent is ended, child.started_at < parent.ended_at, so _BRANCH_CHILD_SQL's timestamp fallback no longer classifies the fork as a branch child and the default GET /api/sessions dropped it. Persist the explicit marker the CLI /branch path already writes; test covers listing + the failed-fork parent survival. --- gateway/platforms/api_server.py | 6 +++++- tests/gateway/test_session_api.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/gateway/platforms/api_server.py b/gateway/platforms/api_server.py index 5c6c8c5d51..5ac4d9a57a 100644 --- a/gateway/platforms/api_server.py +++ b/gateway/platforms/api_server.py @@ -2982,9 +2982,13 @@ class APIServerAdapter(OpenAICompatRoutesMixin, BasePlatformAdapter): # CLI /branch semantics: create the child, then end the original as branched (child first, so # a failed create never leaves the source ended with no fork, #11030). + # ``_branched_from`` is the durable branch marker (same as CLI /branch): with the child created + # first, the timestamp fallback in _BRANCH_CHILD_SQL (child.started_at >= parent.ended_at) no + # longer holds, and an unmarked child would vanish from default session listings. await asyncio.to_thread( db.create_session, fork_id, "api_server", model=source.get("model"), - system_prompt=source.get("system_prompt"), parent_session_id=source_id) + system_prompt=source.get("system_prompt"), parent_session_id=source_id, + model_config={"_branched_from": source_id}) await asyncio.to_thread(db.end_session, source_id, "branched") messages = await asyncio.to_thread(db.get_messages, source_id) await asyncio.to_thread(db.replace_messages, fork_id, messages) diff --git a/tests/gateway/test_session_api.py b/tests/gateway/test_session_api.py index 0064e97e39..4382cb6107 100644 --- a/tests/gateway/test_session_api.py +++ b/tests/gateway/test_session_api.py @@ -116,6 +116,25 @@ async def test_session_messages_default_to_latest_bounded_page(adapter, session_ ] +@pytest.mark.asyncio +async def test_forked_session_stays_listable_and_parent_survives_failed_fork(adapter, session_db): + """A fork is created before the parent is ended (#11030) and carries the explicit branch + marker, so it still shows in the default listing (the timestamp fallback no longer holds).""" + session_db.create_session("parent", "api_server") + app = _create_session_app(adapter) + async with TestClient(TestServer(app)) as cli: + resp = await cli.post("/api/sessions/parent/fork", json={"id": "child"}) + assert resp.status == 201 + listed = await (await cli.get("/api/sessions")).json() + ids = {row["id"] for row in listed["data"]} + assert {"parent", "child"} <= ids, ids + + session_db.create_session("solo", "api_server") + with patch.object(session_db, "create_session", side_effect=RuntimeError("boom")): + resp = await cli.post("/api/sessions/solo/fork", json={"id": "never"}) + assert resp.status >= 500 + assert session_db.get_session("solo")["end_reason"] is None + @pytest.mark.asyncio async def test_run_agent_binds_api_session_context_for_tool_env(adapter, monkeypatch): """API-server request sessions should reach tools and terminal subprocess env."""