Conversation
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
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
…nts" This reverts commit 02a6dba.
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
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
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
A review-bot finding on the polling predicate this commit introduced. Sentry parks a brand-new run at awaiting_input before it has finished creating it in Seer: the create is dispatched after the transaction commits, and until it lands the run carries no Seer id. Treating that status as stopped meant the queries went quiet during exactly the window where the create can still fail the run or rewrite the projection, leaving the UI on awaiting input until a reload. The predicate now takes whether the Seer run exists, so a placeholder awaiting_input keeps being read while a genuine one -- blocked on a person -- still does not. A create that fails leaves no Seer id behind but flips the run to failed, which is terminal, so the missing id cannot restart polling on a stopped run. The detail view reads a summary that carries no run id, so it keeps the blocked-on-a-person reading. Claude-Session: https://claude.ai/code/session_014zh69vex76pjNTnarqVcXL
5a2c9ef to
248163c
Compare
wedamija
left a comment
There was a problem hiding this comment.
Three notes on the freeze logic. The window arithmetic itself is right — reference_time is the end of the window, and each call site anchors on the execution that produced the data. The two blocking items are that the new fingerprint test does not reach the new code, and that the filters fallback can pair a current duration with an old anchor.
Review by Claude Code.
| assert snapshot["context"][0]["queryContext"]["source"] == source | ||
| assert snapshot["context"][0]["result"]["queryLinks"] == [link] | ||
|
|
||
| def test_relative_filters_keep_the_same_request_fingerprint(self) -> None: |
There was a problem hiding this comment.
This test passes on master. create_block() makes no execution, so _query_refinement_context_execution returns None and the freeze path never runs. build_block_execution_snapshot also never calls timezone.now(), so both mocks are no-ops.
Add a completed previous execution with statsPeriod in its input_snapshot, then build the snapshot twice at different wall-clock times. That is the case that has to stay stable.
via Claude Code
| previous_input = previous_execution.input_snapshot | ||
| query_context = previous_input.get("queryContext") or { | ||
| "source": previous_input.get("source", source), | ||
| "filters": previous_input.get("filters", filters), |
There was a problem hiding this comment.
The filters default is the investigation's filters now, but reference_time is the previous execution's start. _upsert_report_block writes no filters, so refining a report query block pairs today's duration with yesterday's anchor. The resulting window looks absolute and exact, and no query used it.
Suggest omitting the window when previous_input has no filters, and letting source.timeRange / analysisWindow supply it.
via Claude Code
| query_context = previous_input.get("queryContext") or { | ||
| "source": previous_input.get("source", source), | ||
| "filters": previous_input.get("filters", filters), | ||
| "parameters": previous_input.get("parameters", parameters), |
There was a problem hiding this comment.
parameters come from the previous execution here, but snapshot["parameters"] holds the new values, and the prompt gives queryContext precedence. A user who edits a parameter and re-runs then gets the old value. Is freezing the parameters intended, or only the window?
via Claude Code
Query refinements retain the original query links when reusing saved data, and carry forward the source, filters, and parameters through subsequent refinements. Relative query windows become absolute dates anchored to the producing execution; existing absolute bounds are preserved. Agent-generated report cells also save their source context so refinement can recover the investigation window when no query link exists.
The query instructions require presentation changes to reuse saved measurements and query changes to preserve the historical window unless the user explicitly requests another period. Regression coverage checks refinement ten days later and a subsequent refinement after that. Links without exact query bounds use the original execution time as their anchor.
Based on #124334. The frontend counterpart is #124516, which reveals ready cells and removes Rerun.