Skip to content

feat(investigations): Add agentic orchestration workflows - #122949

Draft
arslnb wants to merge 8 commits into
masterfrom
sentry/investigations-agentic-orchestration
Draft

feat(investigations): Add agentic orchestration workflows#122949
arslnb wants to merge 8 commits into
masterfrom
sentry/investigations-agentic-orchestration

Conversation

@arslnb

@arslnb arslnb commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Investigations can now run as agent-managed workflows instead of a fixed cell sequence. Sentry persists orchestration runs and events, dispatches lifecycle and steering commands to Seer, projects agent-authored reports into notebook cells, and renders live hypothesis progress plus evidence previews. Users can monitor, cancel, retry, and steer active investigations while existing manual and template investigations keep their current behavior.

The branch is rebased onto current master and preserves the latest Explore routes, feedback entry point, collapsed evidence behavior, cell action menus, access controls, and shared Seer embed formats. The obsolete react-mentions dependency is no longer part of the branch; @xyflow/react remains because it powers the workflow graph.

This draft intentionally keeps the complete end-to-end implementation together for review. It spans backend/migrations and frontend code, so it must be split along the deployment boundary before landing, with persistence and APIs landing before the UI.

@github-actions github-actions Bot added Scope: Frontend Automatically applied to PRs that change frontend components Scope: Backend Automatically applied to PRs that change backend components labels Aug 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🚨 Warning: This pull request contains Frontend and Backend changes!

It's discouraged to make changes to Sentry's Frontend and Backend in a single pull request. The Frontend and Backend are not atomically deployed. If the changes are interdependent of each other, they must be separated into two pull requests and be made forward or backwards compatible, such that the Backend or Frontend can be safely deployed independently.

Have questions? Please ask in the #discuss-dev-infra channel.

@github-actions

Copy link
Copy Markdown
Contributor

Story previews

Preview the stories changed in this PR on the Vercel deployment:

Preview deployment: https://sentry-9e6o2oxxv.sentry.dev

@github-actions

Copy link
Copy Markdown
Contributor

📊 Type Coverage Diff

Metric Before After Delta
Coverage 95.30% 95.31% 🟢 +0.01%
Typed 137,052 137,488 🟢 +436
Untyped 6,753 6,762 🔴 +9
🔍 9 new type safety issues introduced

Non-null assertions (!) (5 new)

File Line Detail
static/app/components/seer/markdown/embeds/components/event.tsx 31 issueId!
static/app/components/seer/markdown/embeds/components/event.tsx 36 eventId!
static/app/views/investigations/detail/orchestrationWorkflow.tsx 400 activeActivity.at(-1)!
static/app/views/investigations/detail/orchestrationWorkflow.tsx 859 activity.at(-1)!
static/app/views/investigations/detail/orchestrationWorkflow.tsx 893 activity.at(-1)!

Type assertions (as) (4 new)

File Line Detail
static/app/components/seer/markdown/embeds/components/event.tsx 41 as Eventquery.data as Event
static/app/components/seer/markdown/embeds/components/event.tsx 42 as EventIdResponsequery.data as EventIdResponse
static/app/components/seer/markdown/embeds/components/event.tsx 46 `as EventIdResponse
static/app/components/seer/markdown/embeds/components/event.tsx 55 as Levelevent.level as Level

This is informational only and does not block the PR.

@github-actions

Copy link
Copy Markdown
Contributor

This PR has a migration; here is the generated SQL for src/sentry/investigations/migrations/0006_add_investigation_orchestration.py

for 0006_add_investigation_orchestration in investigations

--
-- Add field producing_seer_run_id to investigationblock
--
ALTER TABLE "investigations_investigationblock" ADD COLUMN "producing_seer_run_id" bigint NULL;
--
-- Add field report_revision to investigationblock
--
ALTER TABLE "investigations_investigationblock" ADD COLUMN "report_revision" integer NULL CHECK ("report_revision" >= 0);
--
-- Add field stable_agent_key to investigationblock
--
ALTER TABLE "investigations_investigationblock" ADD COLUMN "stable_agent_key" varchar(128) NULL;
--
-- Create model InvestigationOrchestrationRun
--
CREATE TABLE "investigations_investigationorchestrationrun" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "date_updated" timestamp with time zone NOT NULL, "date_added" timestamp with time zone NOT NULL, "seer_run_id" bigint NULL UNIQUE, "schema_version" integer DEFAULT 1 NOT NULL CHECK ("schema_version" >= 0), "workflow_version" integer DEFAULT 1 NOT NULL CHECK ("workflow_version" >= 0), "generation" integer DEFAULT 1 NOT NULL CHECK ("generation" >= 0), "phase" varchar(32) DEFAULT 'intake' NOT NULL, "status" varchar(32) DEFAULT 'pending' NOT NULL, "source" jsonb DEFAULT '{}'::jsonb NOT NULL, "projection" jsonb DEFAULT '{}'::jsonb NOT NULL, "notebook_revision" integer DEFAULT 0 NOT NULL CHECK ("notebook_revision" >= 0), "last_event_sequence" integer DEFAULT 0 NOT NULL CHECK ("last_event_sequence" >= 0), "heartbeat_at" timestamp with time zone NULL, "error" jsonb NULL, "investigation_id" bigint NOT NULL UNIQUE);
--
-- Create model InvestigationOrchestrationEvent
--
CREATE TABLE "investigations_investigationorchestrationevent" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "date_updated" timestamp with time zone NOT NULL, "date_added" timestamp with time zone NOT NULL, "event_id" uuid NOT NULL, "sequence" integer NOT NULL CHECK ("sequence" >= 0), "type" varchar(64) NOT NULL, "payload" jsonb DEFAULT '{}'::jsonb NOT NULL, "application_status" varchar(32) DEFAULT 'pending' NOT NULL, "error" jsonb NULL, "applied_at" timestamp with time zone NULL, "orchestration_run_id" bigint NOT NULL);
--
-- Create model InvestigationOrchestrationCommand
--
CREATE TABLE "investigations_investigationorchestrationcommand" ("id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "date_updated" timestamp with time zone NOT NULL, "date_added" timestamp with time zone NOT NULL, "request_id" uuid NOT NULL, "actor_id" bigint NULL, "expected_workflow_version" integer NOT NULL CHECK ("expected_workflow_version" >= 0), "resulting_workflow_version" integer NULL CHECK ("resulting_workflow_version" >= 0), "type" varchar(64) NOT NULL, "payload" jsonb DEFAULT '{}'::jsonb NOT NULL, "status" varchar(32) DEFAULT 'accepted' NOT NULL, "error" jsonb NULL, "orchestration_run_id" bigint NOT NULL);
--
-- Add field orchestration_run to investigationblock
--
ALTER TABLE "investigations_investigationblock" ADD COLUMN "orchestration_run_id" bigint NULL;
--
-- Create index investigati_orchest_5b44a3_idx on field(s) orchestration_run, report_revision, stable_agent_key of model investigationblock
--
CREATE INDEX CONCURRENTLY "investigati_orchest_5b44a3_idx" ON "investigations_investigationblock" ("orchestration_run_id", "report_revision", "stable_agent_key");
--
-- Create constraint invest_unique_report_block_key on model investigationblock
--
CREATE UNIQUE INDEX CONCURRENTLY "invest_unique_report_block_key" ON "investigations_investigationblock" ("orchestration_run_id", "report_revision", "stable_agent_key") WHERE "stable_agent_key" IS NOT NULL;
--
-- Create index investigati_status_9ed759_idx on field(s) status, heartbeat_at of model investigationorchestrationrun
--
CREATE INDEX CONCURRENTLY "investigati_status_9ed759_idx" ON "investigations_investigationorchestrationrun" ("status", "heartbeat_at");
--
-- Create index investigati_phase_8930ff_idx on field(s) phase, -date_updated of model investigationorchestrationrun
--
CREATE INDEX CONCURRENTLY "investigati_phase_8930ff_idx" ON "investigations_investigationorchestrationrun" ("phase", "date_updated" DESC);
--
-- Create index investigati_orchest_c79498_idx on field(s) orchestration_run, application_status, sequence of model investigationorchestrationevent
--
CREATE INDEX CONCURRENTLY "investigati_orchest_c79498_idx" ON "investigations_investigationorchestrationevent" ("orchestration_run_id", "application_status", "sequence");
--
-- Create constraint invest_orch_unique_event_id on model investigationorchestrationevent
--
CREATE UNIQUE INDEX CONCURRENTLY "invest_orch_unique_event_id" ON "investigations_investigationorchestrationevent" ("orchestration_run_id", "event_id");
ALTER TABLE "investigations_investigationorchestrationevent" ADD CONSTRAINT "invest_orch_unique_event_id" UNIQUE USING INDEX "invest_orch_unique_event_id";
--
-- Create constraint invest_orch_unique_event_sequence on model investigationorchestrationevent
--
CREATE UNIQUE INDEX CONCURRENTLY "invest_orch_unique_event_sequence" ON "investigations_investigationorchestrationevent" ("orchestration_run_id", "sequence");
ALTER TABLE "investigations_investigationorchestrationevent" ADD CONSTRAINT "invest_orch_unique_event_sequence" UNIQUE USING INDEX "invest_orch_unique_event_sequence";
--
-- Create index investigati_orchest_64e689_idx on field(s) orchestration_run, status, date_added of model investigationorchestrationcommand
--
CREATE INDEX CONCURRENTLY "investigati_orchest_64e689_idx" ON "investigations_investigationorchestrationcommand" ("orchestration_run_id", "status", "date_added");
--
-- Create constraint invest_orch_unique_command_request on model investigationorchestrationcommand
--
CREATE UNIQUE INDEX CONCURRENTLY "invest_orch_unique_command_request" ON "investigations_investigationorchestrationcommand" ("orchestration_run_id", "request_id");
ALTER TABLE "investigations_investigationorchestrationcommand" ADD CONSTRAINT "invest_orch_unique_command_request" UNIQUE USING INDEX "invest_orch_unique_command_request";
ALTER TABLE "investigations_investigationorchestrationrun" ADD CONSTRAINT "investigations_inves_investigation_id_27bb2b36_fk_investiga" FOREIGN KEY ("investigation_id") REFERENCES "investigations_investigation" ("id") DEFERRABLE INITIALLY DEFERRED NOT VALID;
ALTER TABLE "investigations_investigationorchestrationrun" VALIDATE CONSTRAINT "investigations_inves_investigation_id_27bb2b36_fk_investiga";
ALTER TABLE "investigations_investigationorchestrationevent" ADD CONSTRAINT "investigations_inves_orchestration_run_id_a646a18e_fk_investiga" FOREIGN KEY ("orchestration_run_id") REFERENCES "investigations_investigationorchestrationrun" ("id") DEFERRABLE INITIALLY DEFERRED NOT VALID;
ALTER TABLE "investigations_investigationorchestrationevent" VALIDATE CONSTRAINT "investigations_inves_orchestration_run_id_a646a18e_fk_investiga";
CREATE INDEX CONCURRENTLY "investigations_investigati_orchestration_run_id_a646a18e" ON "investigations_investigationorchestrationevent" ("orchestration_run_id");
ALTER TABLE "investigations_investigationorchestrationcommand" ADD CONSTRAINT "investigations_inves_orchestration_run_id_9613235b_fk_investiga" FOREIGN KEY ("orchestration_run_id") REFERENCES "investigations_investigationorchestrationrun" ("id") DEFERRABLE INITIALLY DEFERRED NOT VALID;
ALTER TABLE "investigations_investigationorchestrationcommand" VALIDATE CONSTRAINT "investigations_inves_orchestration_run_id_9613235b_fk_investiga";
CREATE INDEX CONCURRENTLY "investigations_investigati_actor_id_7438bdc7" ON "investigations_investigationorchestrationcommand" ("actor_id");
CREATE INDEX CONCURRENTLY "investigations_investigati_orchestration_run_id_9613235b" ON "investigations_investigationorchestrationcommand" ("orchestration_run_id");
ALTER TABLE "investigations_investigationblock" ADD CONSTRAINT "investigations_inves_orchestration_run_id_c90475ca_fk_investiga" FOREIGN KEY ("orchestration_run_id") REFERENCES "investigations_investigationorchestrationrun" ("id") DEFERRABLE INITIALLY DEFERRED NOT VALID;
ALTER TABLE "investigations_investigationblock" VALIDATE CONSTRAINT "investigations_inves_orchestration_run_id_c90475ca_fk_investiga";
CREATE INDEX CONCURRENTLY "investigations_investigationblock_orchestration_run_id_c90475ca" ON "investigations_investigationblock" ("orchestration_run_id");

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Backend Test Failures

Failures on d0797dd in this run:

tests/sentry/investigations/endpoints/test_organization_investigation_base.py::OrganizationInvestigationsEndpointTest::test_detail_and_list_require_result_project_accesslog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/investigations/endpoints/test_organization_investigation_base.py:145: in test_detail_and_list_require_result_project_access
    listed = next(item for item in list_response.data if item["id"] == str(investigation.id))
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E   StopIteration
tests/sentry/seer/endpoints/test_organization_agent_token.py::AgentTokenPublicGetMatrixTest::test_public_mutation_102_OrganizationGroupSearchViews_post_sessionlog
[gw0] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_organization_agent_token.py:2258: in test_matrix_cell
    self._assert_public_mutation_authentication(endpoint, authentication)
tests/sentry/seer/endpoints/test_organization_agent_token.py:2066: in _assert_public_mutation_authentication
    assert baseline.status_code < 400, (
E   AssertionError: ('OrganizationGroupSearchViewsEndpoint', 'POST', b'')
E   assert 404 < 400
E    +  where 404 = <Response status_code=404>.status_code
tests/sentry/seer/endpoints/test_organization_agent_token.py::AgentTokenPublicGetMatrixTest::test_public_mutation_107_OrganizationGroupSearchViews_post_approved_agent_tokenlog
[gw0] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_organization_agent_token.py:2258: in test_matrix_cell
    self._assert_public_mutation_authentication(endpoint, authentication)
tests/sentry/seer/endpoints/test_organization_agent_token.py:2066: in _assert_public_mutation_authentication
    assert baseline.status_code < 400, (
E   AssertionError: ('OrganizationGroupSearchViewsEndpoint', 'POST', b'')
E   assert 404 < 400
E    +  where 404 = <Response status_code=404>.status_code
tests/sentry/seer/endpoints/test_organization_agent_token.py::AgentTokenPublicGetMatrixTest::test_public_mutation_106_OrganizationGroupSearchViews_post_scoped_down_agent_tokenlog
[gw0] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_organization_agent_token.py:2258: in test_matrix_cell
    self._assert_public_mutation_authentication(endpoint, authentication)
tests/sentry/seer/endpoints/test_organization_agent_token.py:2066: in _assert_public_mutation_authentication
    assert baseline.status_code < 400, (
E   AssertionError: ('OrganizationGroupSearchViewsEndpoint', 'POST', b'')
E   assert 404 < 400
E    +  where 404 = <Response status_code=404>.status_code
tests/sentry/seer/endpoints/test_organization_agent_token.py::AgentTokenPublicGetMatrixTest::test_public_mutation_104_OrganizationGroupSearchViews_post_viewer_contextlog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_organization_agent_token.py:2258: in test_matrix_cell
    self._assert_public_mutation_authentication(endpoint, authentication)
tests/sentry/seer/endpoints/test_organization_agent_token.py:2066: in _assert_public_mutation_authentication
    assert baseline.status_code < 400, (
E   AssertionError: ('OrganizationGroupSearchViewsEndpoint', 'POST', b'')
E   assert 404 < 400
E    +  where 404 = <Response status_code=404>.status_code
tests/sentry/seer/endpoints/test_organization_agent_token.py::AgentTokenPublicGetMatrixTest::test_public_mutation_105_OrganizationGroupSearchViews_post_agent_tokenlog
[gw0] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_organization_agent_token.py:2258: in test_matrix_cell
    self._assert_public_mutation_authentication(endpoint, authentication)
tests/sentry/seer/endpoints/test_organization_agent_token.py:2066: in _assert_public_mutation_authentication
    assert baseline.status_code < 400, (
E   AssertionError: ('OrganizationGroupSearchViewsEndpoint', 'POST', b'')
E   assert 404 < 400
E    +  where 404 = <Response status_code=404>.status_code
tests/sentry/seer/endpoints/test_organization_agent_token.py::AgentTokenPublicGetMatrixTest::test_public_mutation_103_OrganizationGroupSearchViews_post_user_tokenlog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_organization_agent_token.py:2258: in test_matrix_cell
    self._assert_public_mutation_authentication(endpoint, authentication)
tests/sentry/seer/endpoints/test_organization_agent_token.py:2066: in _assert_public_mutation_authentication
    assert baseline.status_code < 400, (
E   AssertionError: ('OrganizationGroupSearchViewsEndpoint', 'POST', b'')
E   assert 404 < 400
E    +  where 404 = <Response status_code=404>.status_code
tests/sentry/investigations/endpoints/serializers/test_investigation.py::InvestigationSerializerTest::test_serializes_the_list_representationlog
[gw0] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/investigations/endpoints/serializers/test_investigation.py:28: in test_serializes_the_list_representation
    assert result == {
E   AssertionError: assert {'blockCount'...one.utc), ...} == {'blockCount'...one.utc), ...}
E     
E     Omitting 14 identical items, use -vv to show
E     Left contains 1 more item:
E     �[0m{�[33m'�[39;49;00m�[33morchestration�[39;49;00m�[33m'�[39;49;00m: �[94mNone�[39;49;00m}�[90m�[39;49;00m
E     
E     Full diff:
E     �[0m�[90m �[39;49;00m {�[90m�[39;49;00m
E     �[90m �[39;49;00m     'blockCount': 0,�[90m�[39;49;00m
E     �[90m �[39;49;00m     'createdBy': '278',�[90m�[39;49;00m
E     �[90m �[39;49;00m     'dateCreated': datetime.datetime(2026, 8, 27, 23, 5, 5, 916515, tzinfo=datetime.timezone.utc),�[90m�[39;49;00m
E     �[90m �[39;49;00m     'dateUpdated': datetime.datetime(2026, 8, 27, 23, 5, 5, 916484, tzinfo=datetime.timezone.utc),�[90m�[39;49;00m
E     �[90m �[39;49;00m     'id': '6',�[90m�[39;49;00m
E     �[90m �[39;49;00m     'isFavorited': False,�[90m�[39;49;00m
E     �[90m �[39;49;00m     'mode': 'manual',�[90m�[39;49;00m
E     �[91m-     'sourceType': 'manual',�[39;49;00m�[90m�[39;49;00m
E     �[92m+     'orchestration': None,�[39;49;00m�[90m�[39;49;00m
E     �[92m+     'sourceType': InvestigationSourceType.MANUAL,�[39;49;00m�[90m�[39;49;00m
E     �[90m �[39;49;00m     'status': 'active',�[90m�[39;49;00m
E     �[90m �[39;49;00m     'summary': None,�[90m�[39;49;00m
E     �[90m �[39;49;00m     'summaryDescription': None,�[90m�[39;49;00m
E     �[90m �[39;49;00m     'title': 'Latency spike',�[90m�[39;49;00m
E     �[90m �[39;49;00m     'titleGeneration': {�[90m�[39;49;00m
E     �[90m �[39;49;00m         'status': None,�[90m�[39;49;00m
E     �[90m �[39;49;00m     },�[90m�[39;49;00m
E     �[90m �[39;49;00m     'version': 1,�[90m�[39;49;00m
E     �[90m �[39;49;00m }�[90m�[39;49;00m
tests/sentry/investigations/endpoints/test_organization_investigation_candidates.py::OrganizationInvestigationCandidatesTest::test_launch_and_candidate_require_access_to_the_existing_investigationlog
[gw0] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/investigations/endpoints/test_organization_investigation_candidates.py:465: in test_launch_and_candidate_require_access_to_the_existing_investigation
    assert duplicate.status_code == 403
E   assert 200 == 403
E    +  where 200 = <Response status_code=200, "application/json">.status_code

Comment on lines +805 to +831
return active, False
latest_revision = Investigation.objects.filter(
organization=organization,
lineage_key=lineage_key,
).aggregate(latest=Max("source_revision"))["latest"]
try:
investigation, _ = create_agentic_investigation(
organization=organization,
user_id=user_id,
title=title,
source=normalized_source,
orchestration_source=breached_metric_orchestration_source(normalized_source),
project_ids=project_ids,
filters=filters,
lineage_key=lineage_key,
source_revision=(latest_revision or 0) + 1,
)
return investigation, True
except IntegrityError:
if attempt == 2:
active = Investigation.objects.filter(
organization=organization,
lineage_key=lineage_key,
status=InvestigationStatus.ACTIVE,
).first()
if active is not None:
return active, False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agentic lineage reuse returns investigation details without project-access check

When create_agentic_breached_metric_investigation reuses an active investigation, the create endpoint returns full InvestigationDetailsSerializer output without require_investigation_project_access, unlike template reuse. Callers who can resolve the metric source but lack access to every linked or execution-represented project can read that investigation's details.

Evidence
  • This hunk returns an existing active investigation as (active, False) on lineage hit (and again after the final IntegrityError retry).
  • OrganizationInvestigationsIndexEndpoint.post calls this helper for agentic metric_open_period creates and, when created is false, never calls require_investigation_project_access before responding.
  • The same endpoint does enforce that check on template reuse (if not created: require_investigation_project_access(...)).
  • The 200 response still serializes via InvestigationDetailsSerializer, which includes source/filters/projectIds/parameters/blocks; single-investigation GET/PUT paths are otherwise gated by that same project-access helper.

Identified by Warden · security-review · WLC-BTC

Comment on lines +811 to +821
investigation, _ = create_agentic_investigation(
organization=organization,
user_id=user_id,
title=title,
source=normalized_source,
orchestration_source=breached_metric_orchestration_source(normalized_source),
project_ids=project_ids,
filters=filters,
lineage_key=lineage_key,
source_revision=(latest_revision or 0) + 1,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breached-metric agentic create always violates investigation_source_fields_complete

create_agentic_breached_metric_investigation passes source_revision while create_agentic_investigation leaves source_type=manual and source_key=NULL, so every insert fails the check constraint; the IntegrityError retry path then always raises after 3 attempts instead of creating the investigation.

Evidence
  • create_agentic_breached_metric_investigation calls create_agentic_investigation(..., lineage_key=..., source_revision=(latest_revision or 0) + 1).
  • create_agentic_investigation only sets source, filters, lineage_key, and source_revision — not source_type or source_key (defaults: MANUAL / NULL).
  • Model check investigation_source_fields_complete requires MANUAL rows to have both source_key and source_revision NULL, or non-MANUAL rows to have both set.
  • Template path in create_template_investigation correctly sets source_type=BREACHED_METRIC and source_key=legacy_source_key; this agentic path does not, so Postgres raises IntegrityError on every create.

Identified by Warden · sentry-backend-bugs · A2Q-WPH

Comment on lines +1726 to +1730
if run.seer_run_id is not None and run.seer_run_id != seer_run_id:
raise InvestigationOrchestrationEventConflict("Run ID does not match.")
run.seer_run_id = seer_run_id
if authoritative or (
generation >= run.generation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

synchronize_orchestration_projection crashes when an orchestration run is deleted during dispatch

Treat a missing InvestigationOrchestrationRun as a normal no-op throughout the asynchronous dispatch path. The projection synchronizer uses a bare .get() after the Seer request, so deletion during that request window raises DoesNotExist and escapes both dispatch tasks. If the synchronizer returns a missing result, callers must stop before scheduling follow-up work or acknowledging the command; _mark_command_dispatch_acknowledged should also tolerate deletion after projection synchronization.

Evidence
  • synchronize_orchestration_projection performs select_for_update().get(id=orchestration_run_id) without handling InvestigationOrchestrationRun.DoesNotExist.
  • Both orchestration dispatch tasks load the run with .filter(...).first(), call Seer outside the projection transaction, and then invoke the synchronizer using the stale ID.
  • InvestigationDeletionTask deletes the related orchestration run via InvestigationOrchestrationRunDeletionTask, so the row can disappear while the Seer request is in flight.
  • The command path has a second deletion window after synchronization: _mark_command_dispatch_acknowledged uses bare .get() calls, so merely returning from the synchronizer would not fully prevent the task failure.
  • _mark_create_dispatch_failed already treats a missing run as a no-op, demonstrating the intended behavior for asynchronous cleanup races.
Also found at 2 additional locations
  • src/sentry/tasks/seer/investigation.py:99-101
  • src/sentry/tasks/seer/investigation.py:228-229

Identified by Warden · sentry-backend-bugs · 4EM-V9R

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant