Skip to content

feat(investigations): Add the hypothesis row for agentic runs - #124086

Merged
billyvg merged 21 commits into
masterfrom
billyvong/investigations-hypothesis-ui
Sep 16, 2026
Merged

billyvg merged 21 commits into
masterfrom
billyvong/investigations-hypothesis-ui

Conversation

@billyvg

@billyvg billyvg commented Sep 10, 2026

Copy link
Copy Markdown
Member

Renders the hypotheses an agentic investigation is weighing as a row of cards: the statement, where it landed, the confidence behind that, and the checks the agent ran to get there.

Frontend only. No src/ changes.

Where the data comes from

Nothing new is needed on the wire. Seer pushes the whole live state of a run as one projection blob on every orchestration event, Sentry stores it on InvestigationOrchestrationRun.projection, and GET /organizations/{org}/investigations/{id}/orchestration/ serves the latest one. HypothesisSerializer in src/sentry/investigations/contracts.py already carries everything these cards show:

Card Projection field
Headline statement
Paragraph rationale
Status pill effectiveStatus — already folds the agent verdict and any user disposition into the run status
"86% Confidence" confidence, falling back to agentVerdict.confidence
"Evidence checked" rows verificationSteps[].title + .result

So the agent decides what these cards say purely by what it writes into projection.hypotheses. There is no separate signal telling the frontend to render a hypothesis, and no new block kind — hypotheses are derived agent state that churns during a run, which is why they live in one atomically-overwritten blob rather than as InvestigationBlock rows.

Decisions travel back the other way as versioned commands fenced on workflowVersion, so one made against a stale view is rejected rather than applied to a run that has moved on.

Layout

The row reflows on its container rather than the viewport: auto-fit over minmax(260px, 1fr) tracks drops columns whenever the available space stops fitting another readable card. That lets the same component sit in a full-width detail view and in a narrow drawer without a breakpoint prop or a containerType on the parent — and it is why this does not draw the connecting edges the flow-graph mock has.

Relationship to #122949

That branch has an @xyflow/react HypothesisGraph that lays each hypothesis out as a lane of connected nodes, with details behind a modal. This is the alternative for surfaces that need to fit smaller widths, with the evidence inline on the card instead.

Types and API signatures here match the ones on that branch exactly — InvestigationOrchestration, InvestigationHypothesis, investigationOrchestrationQueryOptions(orgSlug, id), useInvestigationOrchestrationCommandMutation(orgSlug, id) — so the two collapse into one rather than conflicting, whichever lands first.

Storybook

InvestigationFixtureApi, the in-memory fake the other investigations stories use, now serves the two orchestration routes and applies commands in memory, so accept / reject / retry stay interactive on the stories page rather than being static props.

Stories cover the row, every status, and the connected component running against that fixture API. The row sits in Storybook.Demo resizable, so reflow is demonstrated by dragging the demo's edge rather than by hardcoded widths.

Card states

The border has two states, not one per status:

effectiveStatus Border
supported, accepted Solid accent — tokens.border.accent.vibrant (#7553FF)
everything else Dashed

A solid accent edge marks the explanation that stands: supported by the evidence, or accepted by a person. Everything else is dashed, because investigating, refuted, inconclusive, cancelled and failed all read the same way to someone scanning the row — not the answer. The status line already says which of them it is, so the border does not repeat it. (#122949 instead maps refuted and rejected to danger.)

Dashed rather than dotted: a dotted hairline is barely visible at this border color.

report.primaryHypothesisId is a shadow rather than a border, so the two signals stay separable — the border says whether a hypothesis is the answer, the lift says which one the report is built around.

The status line keeps semantic content tokens, which resolve to the mock's colors: supported #008900, inconclusive #A45200, refuted/pending muted #6A6772.

getBorder in the layout primitives only ever emits 1px solid, so the dashed case is a small CSS escape on the card; the colors still come from border tokens. Both states are driven by a data-border attribute rather than a styled prop, which keeps them assertable — this repo stubs getComputedStyle to inline styles only, so emotion CSS cannot be asserted in Jest.

Verification

  • pnpm run typecheck clean
  • pnpm run lint:js static/app/views/investigations clean
  • pnpm test-ci static/app/views/investigations — 106 passing, including the story's own contents rendering against the mocked orchestration API

https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL

Renders the hypotheses an agentic investigation is weighing as a row of
cards: the statement, where it landed, the confidence behind that, and the
checks the agent ran to get there.

The data already exists. Seer pushes the whole live state of a run as one
projection blob on every orchestration event, Sentry stores it on
InvestigationOrchestrationRun.projection, and
`/investigations/$id/orchestration/` serves the latest one. So the agent
decides what these cards say purely by what it writes into
`projection.hypotheses` -- there is no separate signal telling the frontend
to render a hypothesis, and no new block kind. Decisions travel back as
versioned commands fenced on workflowVersion, so one made against a stale
view is rejected rather than applied to a run that has moved on.

The row reflows on its container rather than the viewport: auto-fit over
minmax tracks drops columns whenever the available space stops fitting
another readable card. That lets the same component sit in a full-width
detail view and in a narrow drawer without a breakpoint prop, and it is
why this does not draw the connecting edges the flow-graph mock has.

Types and API signatures match the ones in the in-flight orchestration
branch (#122949) so the two collapse into one rather than conflicting.

Stories cover the row, its reflow, every status, and the connected
component running against the existing story fixture API, which now serves
the two orchestration routes and applies commands in memory so accept,
reject, and retry stay interactive on the stories page.

Claude-Session: https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
@github-actions github-actions Bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Sep 10, 2026
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

📊 Type Coverage Diff

Metric Before After Delta
Coverage 95.51% 95.51% ±0%
Typed 140,345 140,435 🟢 +90
Untyped 6,600 6,601 🔴 +1
🔍 1 new type safety issue introduced

Type assertions (as) (1 new)

File Line Detail
static/app/views/investigations/__stories__/investigationFixtureApi.tsx 379 as InvestigationOrchestrationCommanddata.command as InvestigationOrchestrationCommand

This is informational only and does not block the PR.

Four fixes from design review of the hypothesis row.

The evidence list is a `ul` nested inside the row's own `ul`, so browsers
gave it `list-style-type: circle` and drew a marker beside every step,
sitting in the card's padding. Both lists now clear their markers.

The card border carries the verdict rather than only marking the report's
primary hypothesis: accent for supported or user-accepted, dotted while
inconclusive, ordinary otherwise -- refuted included, since ruling
something out is a result rather than a fault. `getBorder` only ever emits
`1px solid`, so the dotted case is CSS; the colors are still border tokens.
It is driven by a data attribute, which also makes it assertable, unlike
emotion styles, which this repo's stubbed getComputedStyle cannot see.

Stories now sit inside `Storybook.Demo`. That excludes their headings from
the page's table of contents, which was listing every hypothesis statement
and, because the same three statements repeat across stories, giving
several entries the same id and marking them all active at once. The demo
also supplies the container query context these cards size against.

The three hardcoded-width reflow stories are gone; `Demo resizable` gives a
drag handle and a live breakpoint readout instead.

Claude-Session: https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
A dotted hairline is barely visible against the default border color, so
the broken edge is dashed now.

It also applies more widely. The border had three states; it has two. A
solid accent edge marks the explanation that stands -- supported by the
evidence, or accepted by a person -- and every other card is dashed.
Investigating, refuted, inconclusive, cancelled and failed all mean the
same thing to someone scanning the row: not the answer. The status line
already carries which of them it is, so the border does not need to.

Claude-Session: https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
Knip flagged fourteen exports nothing outside their own module reads.
Thirteen were module internals -- the status vocabulary behind
`HypothesisStatus`, the polling predicate behind the orchestration query,
and the projection types composed into `InvestigationOrchestration` -- so
they lose the `export` keyword and keep working. Anything that needs one
later can export it then.

`isInvestigationOrchestrationConflictError` was speculative: added to match
the in-flight orchestration branch, called by nothing here. Deleted; it
arrives with the hook that uses it.

`knip --production` separately reports the hypothesis components as
unreachable, which is accurate -- no investigation surface renders the row
yet, only stories and tests do. That is the same situation the config
already records for `autofixChatContext` and the chat blocks, so it gets
the same treatment: one entry point with a TODO. Wiring the row into the
detail view belongs with the surface work, not here, and that file is
being rewritten on #122949.

Claude-Session: https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
Measured against the mock, every value from a token.

The evidence rows sat on a grey surface; in the spec they sit on the card
and are separated by their border alone, so they move to the primary
background. The rationale was muted, which put it in the same register as
the evidence results below it; the spec reads it as body copy, so it takes
the primary content colour and only the results stay muted.

Spacing was uniformly tighter than the spec: card padding lg -> xl, card
gap md -> lg, evidence rows sm/md -> md/lg with a sm gap between them, and
the gap between cards md -> xl, which now matches the card's own padding.

Type sizes and weights were already right and are unchanged. Worth noting
the scale tops out at 500 -- there is no bolder weight token -- so the
statement is as heavy as the system goes.

Claude-Session: https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
Taken from the prototype recording, which shows the row moving through
states the card had been collapsing into one.

A hypothesis in flight is a single effectiveStatus, but the recording names
four moments inside it: Formed, Preparing checks, Checking, and Evidence
checked. The distinction only exists in the verification steps -- whether
any are planned, and whether they have produced anything -- so that is
where it is read from. Only Checking is coloured and keeps its dot moving;
the others are staging posts, not outcomes, and the old code lit all of
them accent. The heading above the steps moves with them, from "Evidence to
check" to "Evidence checked", and a step with nothing yet reads "Awaiting
evidence" rather than naming its queue position.

A step that has produced something now opens, revealing the objective and
method behind it -- fields the projection has always carried and the card
never showed. A step with no result stays a bare row, because a chevron
there would promise a finding that does not exist yet.

Accepted and rejected keep their own labels rather than folding into
supported and refuted, so a decision is never misread as a verdict.
Confidence is lowercase, matching the recording.

Claude-Session: https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
Verified in a browser this time, which caught four things reading the code
did not.

The evidence row's summary was passed as the Disclosure title's children,
so it landed inside a Button: centred, held to one line, and spilling past
the row's right edge. Moving it to `leadingItems` puts it outside the
button, where it lays out as ordinary content and, taking the row's spare
width, pushes the chevron to the edge the prototype has it on. That also
removed the doubled padding, since the Disclosure supplies its own.

The toggle had no accessible name once the summary moved out of it -- a
screen reader heard "button, collapsed" and nothing else. It now says which
check it opens.

The statement and its rationale were running together as one block, and the
story demos were clipping at Storybook.Demo's 512px maximum, so most of
each row was only reachable by scrolling inside the frame.

Claude-Session: https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
The row talked to the real endpoints already, but nothing mounted it, so it
never appeared in the product. `knip --production` had been reporting
exactly that and the config carried a TODO entry point to keep it quiet;
both are gone now because the detail view reaches it.

`orchestration` on the investigation is what gates it. The field is already
served inline on the list and detail responses and is null for manual and
template investigations, whose orchestration endpoint 404s. The proof of
concept gated on `investigation.mode === 'agentic'` instead, but no `mode`
field exists -- the summary is the only marker an investigation carries.

The detail query now also polls while a run is live. It previously stopped
as soon as no block was executing, which would freeze the gate: a run that
started after load would never show its row, and one that finished would
keep claiming to be running.
The button went grey with nothing to explain it, and the two reasons it
does so are not guessable from the page.

A missing open period is named outright: the page already lists open
periods, so saying there is none gives nothing away.

The other reason cannot be as specific. `unavailable` from the candidates
endpoint covers an issue that cannot be investigated at all, an existing
investigation in a project the viewer cannot see, and a viewer who may not
create one -- collapsed on purpose, since the resolver it comes from notes
that a caller "should not reveal whether an inaccessible or invalid issue
exists". Distinguishing those in a tooltip would leak precisely that, so
the wording covers them together and points at the cause someone can
actually act on.

Neither reason is ever "still loading": a pending query renders a
placeholder in place of the button, and a failed one renders an alert.

Claude-Session: https://claude.ai/code/session_012CtaiBZtJdz8uMRtUgvbmv
…heses

Frames 4705-4707 of the Seer Investigation design put a status block above
the hypothesis row: one line saying what the agent is doing, with a chip and
an elapsed counter, and the row beneath it inside a shared panel.

The block is one component for the whole lifecycle, because the shape never
changes -- icon, sentence, chip, time -- only the words and the colour do. It
sits in a fixed spot for the life of a run, so a reader who has learned where
to look for "what is happening" never has to relearn it. `status` picks the
variant and `phase` picks the words: every in-flight phase shares one
`processing` status, so the phase is the only thing separating "gathering
context" from "finalizing".

The design's chip turned out to be the existing `Tag`: `variant="info"`
resolves to `content.accent` on `background.transparent.accent.muted`, which
is exactly what the Figma variables name. No new styled component needed.

Two states are not from the frames and are marked as such. `cancelled` is
here because the projection can report it and falling through to `failed`
would paint a decision someone deliberately made bright red. `elapsed` is an
optional prop the wired block leaves empty -- the projection carries no
run-level start time, only per-block `startedAt` -- so it renders in the
story and nowhere else rather than counting from an invented origin.

The cards move with it. A hypothesis being verified now keeps a solid border
instead of a dashed one, because dashing it announces a verdict the agent has
not reached; dashed is reserved for a card that was checked and is not the
answer. "Checking" becomes "Verifying..." and is drawn as a spinning ring
rather than a pulsing dot, and `refuted` reads amber rather than muted -- a
hypothesis the agent tested and closed is not an error, but it is a result
worth registering as you scan the row.

Claude-Session: https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
Neither entry point produced an investigation with hypotheses. The server
decides from the request body -- a `source` with no `templateKey` builds an
agentic run, anything else builds a plain notebook -- and both buttons sent
a shape that landed elsewhere.

"Investigate" on a metric issue now sends the metric snapshot alone,
dropping the template key. The candidates endpoint already matches agentic
and template lineage keys alike, so a breach that was investigated before
still resolves to "View" rather than offering a duplicate.

"New investigation" sends a manual source. That run opens `awaiting_input`
and stays there until someone supplies a prompt, which nothing in the UI
does yet, so the investigation behaves as before -- an empty notebook --
with an idle run attached.

That idle run is why polling changed. The predicate asked whether a run had
reached a terminal status, and `awaiting_input` has not; left alone, every
newly created investigation would have polled the detail and orchestration
endpoints every two seconds forever. It now asks whether the agent is
advancing, which `awaiting_input` is not: it is blocked on a person, and
supplying input writes the new projection into the cache and starts it
again.

Note that this does not gate the behaviour behind anything beyond the
existing organizations:investigations flag -- every investigation created
in a flagged org becomes agentic.

Claude-Session: https://claude.ai/code/session_012CtaiBZtJdz8uMRtUgvbmv
The agent writes its findings in terms of what it read, so they are mostly symbols: module/file.py::function_name, dotted paths, issue short IDs. None of them carry a break opportunity, so a token wider than the column pushed its own text out through the card edge instead of wrapping inside it.

Set the scraps wordBreak prop on every agent-written string in the card, not just the evidence detail that surfaced it.

Claude-Session: https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
Drop the block count from the subheader, the status badge and the Seer mark from the header actions, and the debug-only add text/query cell composer.

The main content area now fills the page and left-aligns with the rest of the product rather than sitting in a centred 884px column. Three separate caps were producing that column: the header grid, the canvas wrapper, and the two stacks around the hypothesis row and the notebook.

Removing the composer left useAddInvestigationBlockMutation with no callers, so it goes too. The specs that reached the behaviour through it now exercise it directly: the never-run cell test uses the fixture's own unrun query block, and the fixture-API mutation test drives a rename instead.

Claude-Session: https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
@billyvg

billyvg commented Sep 14, 2026

Copy link
Copy Markdown
Member Author

bugbot review

@cursor cursor Bot left a comment

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.

Stale Bugbot comment from a previous run.

Comment thread static/app/views/investigations/hypotheses/investigationHypotheses.tsx Outdated
Comment thread static/app/views/investigations/hypotheses/hypothesisStatus.tsx
Reverts the fluid-width half of 45de482. Running the notebook to the full page width stretched the prose past a comfortable measure, and body text is most of what this view renders.

The header grid, the canvas, the hypothesis row and the notebook column go back to the centred 884px column. Everything else from that commit stands: the block count, the status badge, the Seer mark and the debug-only add-cell composer stay gone.

Claude-Session: https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
Two defects the review bot found in the hypothesis row.

A settled run stops polling, but accept, reject and retry stay on the menu -- and acting on a finished run is the main reason to open it. Sentry only queues a command: the response carries the projection it already had with nothing but workflowVersion moved on, and Seer rewrites the real one later. The card therefore kept its old disposition until someone reloaded. An accepted command now reopens polling for a bounded window, long enough for Seer to apply it and short enough not to poll a stopped run forever.

Separately, verificationSteps is declared required=False with no default on the contract, so DRF omits the key rather than sending an empty list. The frontend type claimed it was always there, and two call sites read .length and .filter straight off it -- a hypothesis the agent has only just formed would crash the row it appears in. The type now says what the wire says, which is what surfaced the third unguarded caller.

Claude-Session: https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
@billyvg

billyvg commented Sep 15, 2026

Copy link
Copy Markdown
Member Author

bugbot review

@cursor cursor Bot left a comment

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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 6365e30. Configure here.

Comment on lines -371 to -372
<Text variant="muted">{t('%s blocks', investigation.blockCount)}</Text>
<MetaDivider />

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

removed bc it wasn't in the mocks, but could add it back is we think it's useful

Comment on lines -396 to -398
<Badge variant={getStatusVariant(investigation.status)}>
{formatStatus(investigation.status)}
</Badge>

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

there's another badge elsewhere

Comment on lines +393 to +394
* being present is the only thing that says one is: it is null for
* manual and template investigations, whose orchestration endpoint

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This isn't true about manual/template as we're going to change it to be agentic.

Comment on lines -430 to -435
{investigation.status === 'active' ? (
<AddCellComposer
isAdding={addBlockMutation.isPending}
onAdd={handleAddBlock}
/>
) : null}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Intentional, no longer needed (it was for v0 and debugging)

@billyvg
billyvg marked this pull request as ready for review September 15, 2026 17:44
@billyvg
billyvg requested a review from a team as a code owner September 15, 2026 17:44
@billyvg
billyvg requested a review from ryan953 September 15, 2026 18:09
another readable card — the viewport is never consulted. Drag the demo's edge to
watch it reflow.
</p>
<Storybook.Demo resizable direction="column" align="stretch">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

TIL resizable

* apply, and supplies them here. No menu renders when this is empty.
*/
actions?: MenuItemProps[];
className?: string;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

className looks unused

</Text>
<EvidenceList as="ul" gap="sm" padding="0">
{steps.map(step => (
<VerificationStepRow key={step.id} step={step} />

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

noticed poop click target on this story: /scraps/product/views/investigations/hypotheses/hypotheses/

Image

seems like not a proper Disclosure in there?

Comment on lines +39 to +41
function humanize(status: string): string {
return status.replaceAll('_', ' ').replace(/^./, character => character.toUpperCase());
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@seer extract this to utils/string/ folder

Comment on lines +43 to +47
return t(
'%s • %s',
tn('%s possible cause', '%s possible causes', causeCount),
tn('%s check completed', '%s checks completed', checkCount)
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Per review. `humanize` was local to the hypothesis status module, but there is
nothing investigation-specific about it: it is the fallback any open-set wire
value needs when the frontend meets a name it does not have a translated label
for.

It sits alongside `capitalize` rather than reusing it, because the rest of the
value is deliberately left alone -- an acronym the API sent in caps reads better
kept that way, and `capitalize` lowercases everything after the first letter.

Claude-Session: https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
Per review: the toggle on an evidence row was a ~24px chevron on a row several
hundred pixels wide.

`Disclosure.Title` renders `leadingItems` outside its button, so putting the
step's summary there left the chevron alone inside it. The summary is now the
title's children, which is what the component expects: one full-width stretched
button spanning the row, named by the text it contains rather than by a separate
aria-label. The chevron moves to the left of the summary, where every other
Disclosure in the app puts it.

The reason the summary was in the leading slot still holds -- `Button` is sized
as a single-line control, so a two-line block of title-plus-result needs the
fixed height, `nowrap` and centred contents overridden to sit inside one. That
is what the styled title does, and `&&` keeps those rules from depending on
emotion's insertion order against the button's own class.

While here, drop the card's `className` prop: nothing passes one.

Claude-Session: https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
Per review. The run tally was assembled with `t('%s • %s', ...)`, which puts a
format string whose only content is a bullet into the catalog. Each half is
already translated on its own; the bullet between them is punctuation, so the
two are joined directly.

Claude-Session: https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
CI failure, from a rule that landed on master after this branch: scraps'
`require-render-prop-spread` rejects destructuring the parameter a scraps render
function receives.

The status line took `{({className}) => ...}` and put it on the `Flex`. Today
that is the whole object -- `Text` calls its render function with exactly
`{className}` -- but naming the one prop is what the rule is about: the callee
decides what it hands down, and a destructure silently drops anything added to
it later. Spreading forwards whatever arrives.

Nothing about the rendered output changes, which is why the status specs are
untouched.

Claude-Session: https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
@billyvg
billyvg merged commit b1fa029 into master Sep 16, 2026
83 of 123 checks passed
@billyvg
billyvg deleted the billyvong/investigations-hypothesis-ui branch September 16, 2026 17:26
billyvg added a commit that referenced this pull request Sep 16, 2026
…4334)

This changes the metric issue and the new investigation entry points so
that they create the new agentic style investigation (e.g. the one w/
the hypothesis).

## Slop

Neither entry point produced an investigation with hypotheses. The
server decides from the request body — a `source` with no `templateKey`
builds an agentic run, anything else builds a plain notebook — and both
buttons sent a shape that landed elsewhere.

**"Investigate" on a metric issue** now sends the metric snapshot alone,
dropping the template key. The candidates endpoint already matches
agentic and template lineage keys alike, so a breach that was
investigated before still resolves to "View" rather than offering a
duplicate.

**"New investigation"** sends a manual source. That run opens
`awaiting_input` and stays there until someone supplies a prompt, which
nothing in the UI does yet, so the investigation behaves as before — an
empty notebook — with an idle run attached.

That idle run is why polling changed. The predicate asked whether a run
had reached a terminal status, and `awaiting_input` has not; left alone,
every newly created investigation would have polled the detail and
orchestration endpoints every two seconds forever. It now asks whether
the agent is advancing, which `awaiting_input` is not: it is blocked on
a person, and supplying input writes the new projection into the cache
and starts it again.

Note that this does not gate the behaviour behind anything beyond the
existing `organizations:investigations` flag — every investigation
created in a flagged org becomes agentic.

## Base branch

This targets `billyvong/investigations-hypothesis-ui` (#124086) rather
than `master`: it edits
`static/app/views/investigations/hypotheses/investigationHypotheses.tsx`,
which only exists on that branch. It was originally committed there and
reverted so that PR could stay focused; this is that commit,
cherry-picked.

## Visual verification

Blocked, and not skipped silently: this change lives in the request body
sent when an investigation is **created**, so exercising it needs a
local backend to create against. `devservices` is not running (Docker
daemon is down), and the only other reachable API is production, which
is not a valid QA target. Screenshots from the Storybook fixture API
would not be evidence of this change, since the fixtures do not exercise
the creation path.

Happy to capture the real states if `devservices` gets brought up.

## Checks

- `pnpm run typecheck` clean
- `pnpm run lint:js` clean
- 621 tests pass across `static/app/views/investigations/` and
`static/app/views/detectors/`

https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants