fix(core): validate run exists before creating step/step_metrics (#9382)

Checks if the referenced run_id exists in the runs table before
inserting steps and step_metrics. If the run doesn't exist (deleted
or failed creation), sets run_id to None instead of hitting
ForeignKeyViolationError on fk_steps_run_id.

Fixes https://us5.datadoghq.com/error-tracking/issue/a1768774-d691-11f0-9330-da7ad0900000

🐾 Generated with [Letta Code](https://letta.com)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Kian Jones
2026-02-09 09:53:31 -08:00
committed by Caren Thomas
parent 4c753f3f3c
commit 6e0e1cc312

View File

@@ -7,8 +7,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from letta.helpers.singleton import singleton from letta.helpers.singleton import singleton
from letta.log import get_logger
logger = get_logger(__name__)
from letta.orm.errors import NoResultFound from letta.orm.errors import NoResultFound
from letta.orm.message import Message as MessageModel from letta.orm.message import Message as MessageModel
from letta.orm.run import Run as RunModel
from letta.orm.sqlalchemy_base import AccessType from letta.orm.sqlalchemy_base import AccessType
from letta.orm.step import Step as StepModel from letta.orm.step import Step as StepModel
from letta.orm.step_metrics import StepMetrics as StepMetricsModel from letta.orm.step_metrics import StepMetrics as StepMetricsModel
@@ -231,11 +235,15 @@ class StepManager:
except NoResultFound: except NoResultFound:
pass pass
if run_id:
run_exists = await session.get(RunModel, run_id)
if not run_exists:
logger.warning("Step run_id %s references non-existent run, setting to None", run_id)
step_data["run_id"] = None
new_step = StepModel(**step_data) new_step = StepModel(**step_data)
await new_step.create_async(session, no_commit=True, no_refresh=True) await new_step.create_async(session, no_commit=True, no_refresh=True)
pydantic_step = new_step.to_pydantic() pydantic_step = new_step.to_pydantic()
# context manager now handles commits
# await session.commit()
return pydantic_step return pydantic_step
@enforce_types @enforce_types
@@ -593,6 +601,12 @@ class StepManager:
"base_template_id": base_template_id, "base_template_id": base_template_id,
} }
if run_id:
run_exists = await session.get(RunModel, run_id)
if not run_exists:
logger.warning("StepMetrics run_id %s references non-existent run, setting to None", run_id)
metrics_data["run_id"] = None
metrics = StepMetricsModel(**metrics_data) metrics = StepMetricsModel(**metrics_data)
await metrics.create_async(session) await metrics.create_async(session)
return metrics.to_pydantic() return metrics.to_pydantic()