Skip to content

Commit 7c57f61

Browse files
trevor-eclaude
andcommitted
fix(seer): Mark handoffs failed when Seer registration fails entirely
create_seer_run_coding_agent_handoff runs per-repo before the batched store_coding_agent_states_to_seer call. If that batch call raises SeerApiError, Seer never learns about any of these agents at all -- for GitHub Copilot/Claude Code, which discover agents to poll purely by iterating Seer's own copy of them, that means nothing will ever check on these again. The just-created handoff rows would sit at their initial pending/running status forever, looking like they're still in progress. Mark them failed in that case. launch_coding_agents' return value is unaffected -- the provider launch itself genuinely succeeded, so callers should still see it as a success; this only corrects Sentry's own bookkeeping about whether we expect this row to ever update again. A Cursor row marked failed here still gets corrected once its own webhook arrives, since that path doesn't depend on Seer's registration at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent b6f8de1 commit 7c57f61

4 files changed

Lines changed: 98 additions & 3 deletions

File tree

src/sentry/seer/agent/coding_agent_handoff.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
store_coding_agent_states_to_seer,
2727
validate_and_get_integration,
2828
)
29-
from sentry.seer.autofix.coding_agent_handoffs import create_seer_run_coding_agent_handoff
29+
from sentry.seer.autofix.coding_agent_handoffs import (
30+
create_seer_run_coding_agent_handoff,
31+
mark_seer_run_coding_agent_handoffs_failed,
32+
)
3033
from sentry.seer.autofix.utils import CodingAgentState, extract_api_error_message
3134
from sentry.seer.models import SeerApiError, SeerRepoDefinition
3235
from sentry.shared_integrations.exceptions import ApiError
@@ -211,6 +214,12 @@ def launch_coding_agents(
211214
"repos": [f"{r.owner}/{r.name}" for r in repos],
212215
},
213216
)
217+
# Seer never learned about these agents, so nothing will ever poll them
218+
# again -- mark the rows we just created so they don't sit at pending
219+
# forever, looking like they're still in progress.
220+
mark_seer_run_coding_agent_handoffs_failed(
221+
agent_ids=[state.id for state in states_to_store]
222+
)
214223

215224
logger.info(
216225
"explorer.coding_agent.launch_result",

src/sentry/seer/autofix/coding_agent_handoffs.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from __future__ import annotations
77

88
import logging
9+
from collections.abc import Sequence
910

1011
from sentry.models.organization import Organization
1112
from sentry.seer.autofix.utils import (
@@ -15,7 +16,11 @@
1516
update_coding_agent_state,
1617
)
1718
from sentry.seer.endpoints.utils import get_seer_run
18-
from sentry.seer.models.run import SeerRunCodingAgentHandoff, SeerRunCodingAgentHandoffExtras
19+
from sentry.seer.models.run import (
20+
SeerRunCodingAgentHandoff,
21+
SeerRunCodingAgentHandoffExtras,
22+
SeerRunCodingAgentHandoffStatus,
23+
)
1924

2025
logger = logging.getLogger(__name__)
2126

@@ -45,6 +50,28 @@ def create_seer_run_coding_agent_handoff(
4550
logger.exception("seer.coding_agent_handoff.create_failed", extra=log_context)
4651

4752

53+
def mark_seer_run_coding_agent_handoffs_failed(agent_ids: Sequence[str]) -> None:
54+
"""Mark handoffs failed when Seer was never told about them at all. GitHub
55+
Copilot/Claude Code polls only discover an agent by iterating Seer's own copy
56+
of it, so if that registration never happened, nothing will ever check on it
57+
again -- leaving the row at its initial pending/running status would make it
58+
look like it's still in progress forever. (A Cursor row marked failed here
59+
still gets corrected by its own webhook later, since that path doesn't depend
60+
on Seer's registration.)
61+
"""
62+
if not agent_ids:
63+
return
64+
65+
try:
66+
SeerRunCodingAgentHandoff.objects.filter(agent_id__in=agent_ids).update(
67+
status=SeerRunCodingAgentHandoffStatus.FAILED
68+
)
69+
except Exception:
70+
logger.exception(
71+
"seer.coding_agent_handoff.mark_failed_error", extra={"agent_ids": agent_ids}
72+
)
73+
74+
4875
def sync_coding_agent_status(
4976
*,
5077
agent_id: str,

tests/sentry/seer/agent/test_coding_agent_handoff.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from sentry.integrations.cursor.integration import CursorAgentIntegration
1010
from sentry.seer.agent.coding_agent_handoff import _resolve_client, launch_coding_agents
1111
from sentry.seer.autofix.utils import CodingAgentProviderType, CodingAgentState
12-
from sentry.seer.models import SeerRepoDefinition
12+
from sentry.seer.models import SeerApiError, SeerRepoDefinition
1313
from sentry.seer.models.run import SeerRunCodingAgentHandoff
1414
from sentry.shared_integrations.exceptions import ApiError
1515
from sentry.testutils.cases import TestCase
@@ -147,6 +147,31 @@ def test_successful_launch_persists_seer_run_coding_agent_handoff_row(
147147
assert handoff.extras["agent_url"] == "https://cursor.sh/agent"
148148
assert handoff.status == "pending"
149149

150+
@patch("sentry.seer.agent.coding_agent_handoff.store_coding_agent_states_to_seer")
151+
@patch("sentry.seer.agent.coding_agent_handoff.validate_and_get_integration")
152+
def test_marks_handoffs_failed_when_seer_storage_errors(self, mock_validate, mock_store):
153+
"""If Seer never learns about a launched agent at all, GitHub Copilot/Claude
154+
polls will never discover it to check on later -- the row must not sit at
155+
pending forever, looking like it's still in progress."""
156+
self.create_seer_run(self.organization, seer_run_state_id=self.run_id)
157+
mock_store.side_effect = SeerApiError("Seer unavailable", status=503)
158+
installation = FakeCodingAgentInstallation(_state("agent-123"))
159+
mock_validate.return_value = (None, installation)
160+
161+
result = launch_coding_agents(
162+
organization=self.organization,
163+
integration_id=1,
164+
run_id=self.run_id,
165+
prompt="Fix the bug",
166+
repos=[_repo("owner", "repo")],
167+
)
168+
169+
# The provider launch itself still succeeded -- callers should still see it.
170+
assert len(result["successes"]) == 1
171+
172+
handoff = SeerRunCodingAgentHandoff.objects.get(agent_id="agent-123")
173+
assert handoff.status == "failed"
174+
150175
@patch("sentry.seer.agent.coding_agent_handoff.store_coding_agent_states_to_seer")
151176
@patch("sentry.seer.agent.coding_agent_handoff.validate_and_get_integration")
152177
def test_launch_raises_value_error(self, mock_validate, mock_store):

tests/sentry/seer/autofix/test_coding_agent_handoffs.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from sentry.seer.autofix.coding_agent_handoffs import (
55
create_seer_run_coding_agent_handoff,
6+
mark_seer_run_coding_agent_handoffs_failed,
67
sync_coding_agent_status,
78
)
89
from sentry.seer.autofix.utils import (
@@ -72,6 +73,39 @@ def test_noop_when_run_not_found(self, mock_logger: Mock) -> None:
7273
)
7374

7475

76+
class MarkSeerRunCodingAgentHandoffsFailedTest(TestCase):
77+
def setUp(self) -> None:
78+
self.seer_run = self.create_seer_run(
79+
self.organization, type=SeerRunType.FEATURE_RUN, seer_run_state_id=RUN_STATE_ID
80+
)
81+
82+
def test_marks_matching_handoffs_failed(self) -> None:
83+
create_seer_run_coding_agent_handoff(self.organization, RUN_STATE_ID, _state("agent-1"))
84+
create_seer_run_coding_agent_handoff(self.organization, RUN_STATE_ID, _state("agent-2"))
85+
86+
mark_seer_run_coding_agent_handoffs_failed(agent_ids=["agent-1", "agent-2"])
87+
88+
handoffs = SeerRunCodingAgentHandoff.objects.filter(seer_run=self.seer_run)
89+
assert all(h.status == "failed" for h in handoffs)
90+
91+
def test_noop_when_agent_ids_empty(self) -> None:
92+
create_seer_run_coding_agent_handoff(self.organization, RUN_STATE_ID, _state("agent-1"))
93+
94+
mark_seer_run_coding_agent_handoffs_failed(agent_ids=[])
95+
96+
handoff = SeerRunCodingAgentHandoff.objects.get(agent_id="agent-1")
97+
assert handoff.status == "running"
98+
99+
def test_does_not_touch_unrelated_agents(self) -> None:
100+
create_seer_run_coding_agent_handoff(self.organization, RUN_STATE_ID, _state("agent-1"))
101+
create_seer_run_coding_agent_handoff(self.organization, RUN_STATE_ID, _state("agent-2"))
102+
103+
mark_seer_run_coding_agent_handoffs_failed(agent_ids=["agent-1"])
104+
105+
assert SeerRunCodingAgentHandoff.objects.get(agent_id="agent-1").status == "failed"
106+
assert SeerRunCodingAgentHandoff.objects.get(agent_id="agent-2").status == "running"
107+
108+
75109
class SyncCodingAgentStatusTest(TestCase):
76110
def setUp(self) -> None:
77111
self.seer_run = self.create_seer_run(

0 commit comments

Comments
 (0)