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.
This commit is contained in:
teknium1
2026-09-12 08:17:46 -07:00
committed by Teknium
parent d0093a11f0
commit ac54e5e715
2 changed files with 24 additions and 1 deletions
+5 -1
View File
@@ -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)
+19
View File
@@ -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."""