mirror of
https://github.com/usestrix/strix.git
synced 2026-08-20 18:38:57 +02:00
fix: salvage a crashed run's full history into the session so revival loses no context
This commit is contained in:
+31
-1
@@ -30,6 +30,7 @@ from strix.core.inputs import child_initial_input
|
||||
from strix.core.sessions import (
|
||||
enforce_image_budget,
|
||||
open_agent_session,
|
||||
replace_session_items,
|
||||
seed_initial_input,
|
||||
strip_all_images_from_session,
|
||||
)
|
||||
@@ -117,6 +118,29 @@ def _transient_model_retry_delay(attempt: int) -> float:
|
||||
return min(delay, _TRANSIENT_MODEL_RETRY_MAX_DELAY_S)
|
||||
|
||||
|
||||
async def _salvage_stream_to_session(
|
||||
session: Session,
|
||||
pre_run_items: list[Any],
|
||||
stream: Any,
|
||||
agent_id: str,
|
||||
) -> None:
|
||||
"""Persist a crashed run's full history so a revived agent loses no context."""
|
||||
if stream is None:
|
||||
return
|
||||
try:
|
||||
replay = list(stream.to_input_list())
|
||||
except Exception:
|
||||
logger.exception("could not build salvage history for %s", agent_id)
|
||||
return
|
||||
desired = list(pre_run_items) + replay
|
||||
if len(desired) <= len(pre_run_items):
|
||||
return
|
||||
try:
|
||||
await replace_session_items(session, desired)
|
||||
except Exception:
|
||||
logger.exception("salvaging crashed run history failed for %s", agent_id)
|
||||
|
||||
|
||||
async def _seed_and_prepare_first_input(
|
||||
session: Session | None, initial_input: Any, *, start_parked: bool
|
||||
) -> Any:
|
||||
@@ -534,6 +558,8 @@ async def _run_cycle( # noqa: PLR0912, PLR0915
|
||||
compactions = 0
|
||||
model_retries = 0
|
||||
while True:
|
||||
stream: Any = None
|
||||
pre_run_items: list[Any] = []
|
||||
try:
|
||||
await coordinator.mark_running(agent_id)
|
||||
if session is not None:
|
||||
@@ -547,6 +573,8 @@ async def _run_cycle( # noqa: PLR0912, PLR0915
|
||||
await _compact_session(agent, session, run_config, force=False)
|
||||
except Exception:
|
||||
logger.exception("proactive compaction failed for %s", agent_id)
|
||||
with contextlib.suppress(Exception):
|
||||
pre_run_items = list(await session.get_items())
|
||||
stream = Runner.run_streamed(
|
||||
agent,
|
||||
input=input_data,
|
||||
@@ -657,6 +685,8 @@ async def _run_cycle( # noqa: PLR0912, PLR0915
|
||||
if session is not None:
|
||||
input_data = []
|
||||
continue
|
||||
if session is not None:
|
||||
await _salvage_stream_to_session(session, pre_run_items, stream, agent_id)
|
||||
if not interactive:
|
||||
raise
|
||||
if isinstance(exc, MaxTurnsExceeded):
|
||||
@@ -671,7 +701,7 @@ async def _run_cycle( # noqa: PLR0912, PLR0915
|
||||
return None
|
||||
else:
|
||||
await _settle_run_result(coordinator, agent_id, interactive)
|
||||
return stream
|
||||
return cast("RunResultBase | None", stream)
|
||||
|
||||
|
||||
async def _settle_run_result(
|
||||
|
||||
@@ -595,6 +595,44 @@ async def test_run_cycle_parked_parks_instead_of_raising(
|
||||
assert coordinator.errors["root"] == "unexpected explosion"
|
||||
|
||||
|
||||
class _SalvageStream:
|
||||
def __init__(self, replay: list[dict[str, Any]]) -> None:
|
||||
self._replay = replay
|
||||
|
||||
def to_input_list(self) -> list[dict[str, Any]]:
|
||||
return self._replay
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_salvage_stream_to_session_preserves_full_history(tmp_path: Any) -> None:
|
||||
session = SQLiteSession("child", tmp_path / "agents.db")
|
||||
await session.add_items([{"role": "user", "content": "identity + task"}])
|
||||
pre_run = list(await session.get_items())
|
||||
|
||||
# A crash mid-run: the stream produced two turns the SDK never committed.
|
||||
stream = _SalvageStream(
|
||||
[
|
||||
{"role": "assistant", "content": "recon turn 1"},
|
||||
{"role": "assistant", "content": "recon turn 2"},
|
||||
]
|
||||
)
|
||||
await execution._salvage_stream_to_session(session, pre_run, stream, "child")
|
||||
|
||||
stored = [cast("dict[str, Any]", i) for i in await session.get_items()]
|
||||
assert [i["content"] for i in stored] == [
|
||||
"identity + task",
|
||||
"recon turn 1",
|
||||
"recon turn 2",
|
||||
]
|
||||
|
||||
# A crash with nothing new to salvage leaves the session untouched.
|
||||
await execution._salvage_stream_to_session(
|
||||
session, list(await session.get_items()), _SalvageStream([]), "child"
|
||||
)
|
||||
assert len(await session.get_items()) == 3
|
||||
session.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_seed_initial_input_persists_and_is_idempotent(tmp_path: Any) -> None:
|
||||
session = SQLiteSession("child", tmp_path / "agents.db")
|
||||
|
||||
Reference in New Issue
Block a user