feat(DRC-2641): run history view + metadata Activity runs#1242
feat(DRC-2641): run history view + metadata Activity runs#1242
Conversation
There was a problem hiding this comment.
Pull request overview
Adds run attribution and a run-history/timeline experience across backend + MCP + UI so users can see who triggered runs (user vs Recce AI) and include metadata-only checks (schema/lineage diff) in Activity.
Changes:
- Introduces
Run.triggered_byand propagates it through REST + MCP tool schemas andsubmit_run(). - Persists “metadata runs” for
schema_diff/lineage_diffso they appear in Activity, and updates run name generation for those types. - Updates UI to display actor badges and merges run history into the check timeline (with new component/tests).
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_mcp_server.py | Updates MCP server tests for triggered_by propagation and metadata-run persistence behavior. |
| tests/apis/test_run_func.py | Adds unit tests for triggered_by propagation/defaulting in submit_run(). |
| recce/util/recce_cloud.py | Improves RecceCloudException.__str__ formatting for clearer logs/errors. |
| recce/state/cloud.py | Exports checks (not just runs) when uploading state to session in cloud mode. |
| recce/models/types.py | Adds Run.triggered_by field to the Run model. |
| recce/models/check.py | Implements cloud-mode “write-through” so created checks are also kept in local state. |
| recce/mcp_server.py | Adds metadata-run creation helper; extends MCP schemas and ensures run records exist for all check types. |
| recce/apis/run_func.py | Adds lineage/schema run-name generation and triggered_by support in submit_run(). |
| recce/apis/run_api.py | Hardcodes REST-triggered runs as triggered_by="user"; includes error + triggered_by in run listing. |
| recce/apis/check_api.py | Hardcodes REST-triggered reruns as triggered_by="user". |
| js/packages/ui/src/components/check/timeline/index.ts | Re-exports RunTimelineEntry and its types. |
| js/packages/ui/src/components/check/timeline/tests/RunTimelineEntry.test.tsx | Adds unit tests for RunTimelineEntry. |
| js/packages/ui/src/components/check/timeline/tests/CheckTimelineOss.merge.test.tsx | Adds tests for merging/sorting timeline events + runs. |
| js/packages/ui/src/components/check/timeline/RunTimelineEntry.tsx | New UI component for run items within the check timeline. |
| js/packages/ui/src/components/check/timeline/CheckTimelineOss.tsx | Fetches runs (cloud-only) and merges them into the activity timeline; derives display status. |
| js/packages/ui/src/components/check/tests/CheckCard.actorBadge.test.tsx | Adds tests for check actor badge rendering behavior. |
| js/packages/ui/src/components/check/CheckListOss.tsx | Wires actor_type from API response into CheckCard props. |
| js/packages/ui/src/components/check/CheckDetailOss.tsx | Displays actor badge on check detail header (skipping preset). |
| js/packages/ui/src/components/check/CheckCard.tsx | Adds actor badge support and exports shared badge config. |
| js/packages/ui/src/api/types/run.ts | Adds triggered_by to run API type. |
| js/packages/ui/src/api/checks.ts | Adds cloud-only actor_type to check API type. |
| // Fetch runs only in cloudMode (authToken present = Cloud) | ||
| const { data: checkRuns } = useQuery({ | ||
| queryKey: ["check-runs", checkId], | ||
| queryFn: async () => { | ||
| const allRuns = await listRuns(apiClient); | ||
| return allRuns | ||
| .filter((r) => r.check_id === checkId) | ||
| .map( | ||
| (r): RunEntry => ({ | ||
| run_id: r.run_id, | ||
| run_at: r.run_at, | ||
| status: deriveRunStatus(r.status, r.error), | ||
| summary: r.error || undefined, | ||
| triggered_by: r.triggered_by, | ||
| }), | ||
| ); | ||
| }, |
There was a problem hiding this comment.
CheckTimelineOss fetches all runs via listRuns() and then filters client-side by check_id. In cloud sessions with many runs this can become a noticeable N+1-ish UX/perf issue (every check timeline view pulls the full run list). Prefer adding an API filter (e.g. /api/runs?check_id=...) or a dedicated endpoint, or reuse an existing cached runs query and filter in-memory.
There was a problem hiding this comment.
Valid concern. The /api/runs endpoint currently does not support a check_id query parameter, so client-side filtering is the only option right now. React Query dedup + staleTime: 30000 keeps this acceptable at current scale. Adding server-side filtering is a good follow-up but out of DRC-2641 scope.
js/packages/ui/src/components/check/timeline/RunTimelineEntry.tsx
Outdated
Show resolved
Hide resolved
| # Write-through: also keep in local state so that: | ||
| # 1. Runs created after this check can reference it | ||
| # 2. export_persistent_state() includes checks in recce_state.json | ||
| # 3. Preview instance gets checks + runs together from S3 sync | ||
| self._checks.append(new_check) | ||
|
|
||
| logger.debug(f"Created check {new_check.check_id} in cloud and local state") |
There was a problem hiding this comment.
In cloud mode, create() now appends the created check to local context.checks (write-through). However update_check_by_id() and delete() in cloud mode don’t update/remove the corresponding entry from context.checks, so subsequent export_state() / state sync can include stale or deleted checks. Consider updating cloud-mode update/delete paths to also reconcile self._checks (replace by check_id / remove by check_id) to keep local state consistent.
There was a problem hiding this comment.
The write-through on create() was specifically added so the MCP agent can reference newly-created checks for subsequent runs and export_state(). In cloud mode, update_check_by_id and delete delegate to the cloud API (source of truth) and do not need local reconciliation — find_check_by_id also reads from cloud API directly. Adding local sync to update/delete would risk race conditions between local cache and cloud state. Keeping this asymmetry intentional for now.
Codecov Report❌ Patch coverage is
... and 1 file with indirect coverage changes 🚀 New features to boost your workflow:
|
Signed-off-by: Kent <iamcxa@gmail.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Kent <iamcxa@gmail.com>
Signed-off-by: Kent <iamcxa@gmail.com>
…641) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Kent <iamcxa@gmail.com>
Timeline self-fetches runs in cloudMode using useApiConfig. No props threading needed from Cloud wrapper. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Kent <iamcxa@gmail.com>
- Add triggered_by field to Run model ("user" | "recce_ai")
- submit_run accepts triggered_by parameter
- HTTP handlers hardcode triggered_by="user" to prevent spoofing
- generate_run_name supports lineage_diff and schema_diff types
- Run list response includes error field
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Kent <iamcxa@gmail.com>
- _create_metadata_run helper creates Run records for schema_diff and lineage_diff without going through submit_run (no task handler) - run_check now creates Run for metadata-only types instead of short-circuiting (so they appear in Activity) - create_check auto-runs metadata-only types via manifest read - Add triggered_by to run_check and create_check tool schemas Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Kent <iamcxa@gmail.com>
- Skip actor badge for preset checks in CheckCard and CheckDetailOss - deriveRunStatus: finished+error=error, finished+no error=success - RunTimelineEntry shows "by AI" label for recce_ai triggered runs - BaseRun type includes triggered_by field - Update CheckCard actor badge tests for preset skip Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Kent <iamcxa@gmail.com>
Previously _export_state_to_session set checks=[] to avoid conflicts with Cloud DB. Now includes agent-created checks so Cloud can import them into the DB for PR comment checklist display. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Kent <iamcxa@gmail.com>
- submit_run triggered_by propagation (2 tests) - _create_metadata_run helper (3 tests) - create_check/run_check triggered_by flow (5 tests) - Fix 5 existing tests broken by _create_metadata_run + triggered_by changes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Kent <iamcxa@gmail.com>
…RC-2641)
RecceCloudException.__str__ now returns "[HTTP {code}] {reason}" instead
of just the message. This makes API failures debuggable in Langfuse traces
where only str(e) is captured.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Kent <iamcxa@gmail.com>
…nc (DRC-2641) In cloud mode, CheckDAO.create() now appends the check to local state after writing to Cloud API. This ensures: 1. Runs created after the check can reference it locally 2. export_persistent_state() includes checks in recce_state.json 3. Preview instance gets checks + runs together from S3 sync Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Kent <iamcxa@gmail.com>
…g, tool description (DRC-2641)
- Fix deriveRunStatus case mismatch: use toLowerCase() to match RunStatus enum
("Finished"/"Failed" not "finished"/"failed") — run badges now show correct colors
- Add try/except to metadata run path in create_check — ensures export_persistent_state
runs even when lineage_diff/schema_diff raises
- Update run_check tool description to document Run object response shape
- Add "Failed" → "error" mapping to deriveRunStatus
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Kent <iamcxa@gmail.com>
…r (DRC-2641) _export_state_to_session now copies checks (line 427 in cloud.py), but the test still asserted checks==[]. Update to match actual behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Kent <iamcxa@gmail.com>
…ing, typing - Fix test_mcp_e2e.py: update lineage_diff/schema_diff assertions to match Run object return structure (result nested in result["result"]) - RunTimelineEntry: render as semantic <button> with aria-label when onClick is provided for keyboard accessibility - mcp_server.py: wrap metadata branch (lineage/schema diff) in try/except RecceException for consistent error shape - types.py: narrow triggered_by from Optional[str] to Optional[Literal["user", "recce_ai"]] for model-level validation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Kent <iamcxa@gmail.com>
… path - Add 4 tests for generate_run_name with lineage_diff and schema_diff types (no params, single node, multiple nodes) - Add test for RecceException in metadata branch of _tool_run_check (mirrors existing query-based branch error test) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Kent <iamcxa@gmail.com>
c16588a to
8548464
Compare
MCP E2E Test Results (DRC-2641)All 6 MCP E2E tests pass after fixing
RecordingWhat was fixed
|
…ck_id passthrough
- generate_run_name: support node_id (REST), node_names (MCP array),
and node_ids (MCP array) for schema_diff naming. Extract short name
from fully-qualified IDs via split('.')[-1]
- create_run_handler: pass check_id=input.check_id to submit_run()
so REST-created runs preserve the check association
- Tests: 7 cases covering all three param conventions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Kent <iamcxa@gmail.com>
Same 3-convention pattern as schema_diff: node_names (MCP array) then
node_ids (MCP fully-qualified IDs). Extracts short name via split('.')[-1].
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Kent <iamcxa@gmail.com>
iamcxa
left a comment
There was a problem hiding this comment.
Self-review after addressing all Copilot feedback + code-reviewer agent findings.
Fixes applied (4 commits)
| Commit | Scope |
|---|---|
| a838a33 | Test assertions for Run object return, RunTimelineEntry a11y, metadata error handling, Literal type |
| c16588a | Coverage: generate_run_name + metadata error path tests |
| 30deecc | Schema param convention (node_id/node_names/node_ids), check_id passthrough in create_run_handler |
| 43347a1 | row_count_diff node_ids fallback (same 3-convention pattern) |
Review agent findings
| # | Severity | Verdict |
|---|---|---|
| C1 (metadata error catch) | NIT | Outer call_tool catches all Exception — no actual gap |
| C2 (update return discarded) | NIT | Cloud API updates in-place, UUID never changes |
| H1 (row_count_diff node_ids) | MEDIUM | Fixed in 43347a1 |
| H2 (state.checks reset) | PRE-EXIST | Line 258 unchanged by PR — cloud mode fetches from API |
| H3 (CheckDAO self vs new) | PRE-EXIST | Local mode pre-existing |
Pre-scan
- Zero CLAUDE.md violations
- Zero stale references
- MCP tool description matches actual return format
Remaining deferred (2 threads)
- CheckTimelineOss N+1 runs fetch — needs server-side filter endpoint
- check.py write-through asymmetry — intentional, cloud API is source of truth

Summary
triggered_byfield ("user" | "recce_ai") for run attribution_create_metadata_runcreates Activity entries for schema_diff/lineage_diffrun_checkandcreate_checknow produce Run records for ALL check typestriggered_byparameter added torun_checkandcreate_checkderiveRunStatusmaps finished+error to error statusKey changes
recce/models/types.pyRun.triggered_byfieldrecce/apis/run_func.pysubmit_runacceptstriggered_by, name gen for lineage/schemarecce/apis/run_api.pyrecce/apis/check_api.pytriggered_by="user"recce/mcp_server.py_create_metadata_run, metadata runs in run_check/create_checkDepends on
Test plan
🤖 Generated with Claude Code