Skip to content

feat(signals): Spawn fresh report on recurrence - #71981

Merged
Twixes merged 7 commits into
masterfrom
posthog-code/signals-resolved-terminal
Jul 17, 2026
Merged

feat(signals): Spawn fresh report on recurrence#71981
Twixes merged 7 commits into
masterfrom
posthog-code/signals-resolved-terminal

Conversation

@Twixes

@Twixes Twixes commented Jul 17, 2026

Copy link
Copy Markdown
Member

Problem

In the Signals product, a resolved inbox report reopens itself whenever a new matching signal arrives — it re-promotes straight back into research. We want resolved reports to be terminal (one report ↔ one shipped fix). But a recurring issue still deserves attention, and its research should benefit from knowing the same thing was resolved before (it might be a regression, or a new dimension of the same issue).

Raised in this Slack thread.

Changes

  • Resolved reports never receive new signals or re-promote. When a signal would have grouped into a resolved report, assign_and_emit_signal_activity starts a fresh report instead.
  • Removed the resolved → candidate pipeline transition so resolved is enforced as terminal at the model level.
  • The two reports are linked with a new related_to artefact type — an untyped link (just a report id) written symmetrically on both reports, so it's discoverable from either side and the grouping dataset can be reconstructed later, without a model field. Direction isn't tracked (the two rows' timestamps capture it). It's a general link type, usable beyond this flow.
  • The research agent for the new report finds whichever linked report is resolved and is handed its title/summary; the initial research prompt gains a ## Previously resolved report block telling the agent to judge regression vs. new dimension vs. distinct.
  • Frontend: added the related_to label to the activity-log renderer. Resolved reports were already terminal in the UI (no reopen/restore/snooze), so no other UI change was needed.

How did you test this code?

The sandbox has no Postgres, so DB-backed tests were written but run in CI; pure-function tests ran green locally.

  • test_artefact_schemas.py (ran locally): the registry-parity test enforces the new related_to type is registered consistently across the model enum and content-schema map.
  • test_research_prompt.py (ran locally): asserts the ## Previously resolved report block appears when resolved context is passed and is absent otherwise.
  • test_assign_and_emit_signal.py: test_resolved_match_spawns_new_report_and_leaves_resolved_untouched — a signal matching a resolved report creates a new POTENTIAL report, writes the symmetric related_to artefacts, and leaves the resolved report's status/count untouched. Narrowed the three READY/RESOLVED re-promotion tests to READY only.

Docs update

Updated products/signals/backend/report_generation/AGENTS.md to document the artefact-based context path.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Authored by the PostHog Slack app (Claude Code). Skills invoked: /django-migrations and /writing-tests. Iterations, per reviewer feedback in the thread: (1) a grouped_from_resolved_report self-FK → (2) a directional related_report artefact pair → (3) a single untyped related_to artefact (timestamps capture direction; the resolved link is identified by the linked report's status). The reopen decision lives in the assign_and_emit_signal_activity Temporal activity, so it covers both the sequential and parallel grouping paths with no workflow versioning/patch.


Created with PostHog from a Slack thread

…recurrence

Resolved inbox reports no longer reopen. When a new signal would have grouped into an
already-resolved report, we start a fresh report instead and point it at the resolved one via
grouped_from_resolved_report. The research agent for that new report is handed the resolved
report's title/summary so it can judge whether the recurrence is a regression, a new dimension
of the same issue, or distinct.

Generated-By: PostHog Code
Task-Id: 5056349e-21fd-45da-a06b-026d62c95f20
@github-actions

Copy link
Copy Markdown
Contributor

Hey @Twixes! 👋

It looks like your git author email on this PR isn't your @posthog.com address (michael@matloka.com). Since you're on the PostHog team, it's worth pointing your local git author email at your @posthog.com address. Why it matters:

  • Consistent work identity in git history — internal tooling that attributes commits to team members keys off your @posthog.com address.
  • Keeps team contributions easy to tell apart from external community ones when scanning history.

You can fix it for this repo with:

git config user.email "you@posthog.com"

Or set it globally with git config --global user.email "you@posthog.com". No need to redo this PR — just a nudge for next time. 🙂

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Bundle size — 🔺 +23.3 KiB (+0.0%)

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 64.86 MiB · 🔺 +23.3 KiB (+0.0%)

File Size Δ vs base
render-query/src/render-query/render-query.js 21.33 MiB 🔺 +8.6 KiB (+0.0%)
posthog-app/_parent/products/posthog_ai/frontend/components/tool/builtinToolRenderers.js 9.6 KiB 🔺 +3.7 KiB (+61.9%)
posthog-app/_parent/products/stamphog/frontend/scenes/StamphogScene/StamphogScene.js 11.8 KiB 🔺 +1.9 KiB (+19.7%)
posthog-app/src/scenes/surveys/wizard/SurveyWizard.js 68.6 KiB 🟢 -1.5 KiB (-2.1%)

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

Eager graph — within budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.23 MiB · 22 files 🔺 +564 B (+0.0%) ███░░░░░░░ 28.6% of 4.29 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.15 MiB · 2,988 files 🔺 +1.1 KiB (+0.0%) █████████░ 88.1% of 9.25 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
Size File
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
4.5 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
3.9 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.4 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
789 B src/scenes/ChunkLoadErrorBoundary.tsx
762 B src/index.tsx
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
281.3 KiB ../node_modules/.pnpm/posthog-js@1.404.0/node_modules/posthog-js/dist/rrweb.js
267.7 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
235.5 KiB src/taxonomy/core-filter-definitions-by-group.json
223.2 KiB ../node_modules/.pnpm/posthog-js@1.404.0/node_modules/posthog-js/dist/module.js
164.0 KiB src/queries/validators.js
154.3 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
105.8 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
93.2 KiB ../packages/quill/packages/quill/dist/index.js

Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479

Dist folder size — 🔺 +355.6 KiB (+0.0%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 1336.91 MiB · 🔺 +355.6 KiB (+0.0%)

⚠️ Playwright — 1 failed

🎭 Playwright report · View test results →

1 failed test:

  • level facet passes severityLevels to API (chromium)

These issues are not necessarily caused by your changes.
Annoyed by this section? Help fix flakies and failures and it will go green!

⚠️ Backend coverage — 89.0% of changed backend lines covered — 11 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): ██████████████████░░ 89.0% (95 / 106)

File Patch Uncovered changed lines
products/signals/backend/temporal/agentic/report.py 46.7% 152–155, 158, 164–166
products/signals/backend/temporal/grouping.py 86.7% 758–759
products/signals/backend/artefact_schemas.py 88.9% 468

🤖 Agents: add a test covering the lines above, or note why under "How did you test this code?". Machine-readable gap list: the patch-coverage artifact on this run (gh run download 29598089691 -n patch-coverage), or the coverage-data block at the end of this comment.

Per-product line coverage (touched products)
Product Coverage Lines
platform_features ██░░░░░░░░░░░░░░░░░░ 12.1% 7 / 58
batch_exports ████████░░░░░░░░░░░░ 39.7% 8,416 / 21,225
demo ███████████░░░░░░░░░ 56.2% 1,497 / 2,663
warehouse_sources_queue ████████████░░░░░░░░ 59.2% 148 / 250
tasks █████████████░░░░░░░ 67.0% 25,933 / 38,686
data_tools ██████████████░░░░░░ 70.0% 63 / 90
ai_gateway ███████████████░░░░░ 75.0% 9 / 12
signals ████████████████░░░░ 79.3% 19,298 / 24,350
data_modeling ████████████████░░░░ 80.0% 4,834 / 6,045
cdp ████████████████░░░░ 80.7% 3,118 / 3,864
wizard ████████████████░░░░ 82.5% 772 / 936
notebooks █████████████████░░░ 84.8% 6,903 / 8,141
cohorts █████████████████░░░ 86.2% 4,065 / 4,717
agent_platform █████████████████░░░ 86.4% 3,807 / 4,405
actions █████████████████░░░ 86.6% 717 / 828
product_tours █████████████████░░░ 87.5% 1,266 / 1,447
exports ██████████████████░░ 88.4% 6,933 / 7,842
visual_review ██████████████████░░ 88.5% 5,565 / 6,287
business_knowledge ██████████████████░░ 88.5% 4,400 / 4,969
conversations ██████████████████░░ 89.0% 16,183 / 18,186
mcp_analytics ██████████████████░░ 89.2% 2,514 / 2,819
dashboards ██████████████████░░ 89.3% 5,841 / 6,540
engineering_analytics ██████████████████░░ 89.3% 5,407 / 6,053
error_tracking ██████████████████░░ 89.9% 9,758 / 10,854
alerts ██████████████████░░ 89.9% 4,045 / 4,499
early_access_features ██████████████████░░ 90.1% 1,031 / 1,144
streamlit_apps ██████████████████░░ 90.4% 2,501 / 2,767
slack_app ██████████████████░░ 90.6% 9,018 / 9,955
links ██████████████████░░ 90.6% 183 / 202
marketing_analytics ██████████████████░░ 90.8% 11,514 / 12,684
stamphog ██████████████████░░ 91.0% 3,993 / 4,387
product_analytics ██████████████████░░ 91.3% 5,757 / 6,304
mcp_store ██████████████████░░ 91.8% 3,685 / 4,012
data_warehouse ██████████████████░░ 92.4% 18,626 / 20,158
notifications ███████████████████░ 92.7% 1,031 / 1,112
web_analytics ███████████████████░ 92.7% 13,535 / 14,597
workflows ███████████████████░ 92.8% 5,482 / 5,909
ai_observability ███████████████████░ 92.8% 14,916 / 16,077
surveys ███████████████████░ 93.0% 5,724 / 6,157
posthog_ai ███████████████████░ 93.2% 1,325 / 1,421
approvals ███████████████████░ 93.3% 3,395 / 3,640
reminders ███████████████████░ 93.4% 468 / 501
tracing ███████████████████░ 93.4% 2,545 / 2,724
managed_migrations ███████████████████░ 93.8% 1,220 / 1,300
legal_documents ███████████████████░ 94.1% 1,568 / 1,667
endpoints ███████████████████░ 94.1% 8,606 / 9,143
revenue_analytics ███████████████████░ 94.5% 3,598 / 3,809
skills ███████████████████░ 94.5% 2,881 / 3,049
messaging ███████████████████░ 94.5% 2,530 / 2,677
review_hog ███████████████████░ 94.6% 6,789 / 7,174
logs ███████████████████░ 95.2% 9,439 / 9,911
experiments ███████████████████░ 95.7% 24,417 / 25,527
replay_vision ███████████████████░ 95.8% 13,776 / 14,383
growth ███████████████████░ 95.8% 2,837 / 2,960
annotations ███████████████████░ 96.2% 732 / 761
feature_flags ███████████████████░ 96.3% 16,196 / 16,822
user_interviews ███████████████████░ 96.4% 2,242 / 2,325
warehouse_sources ███████████████████░ 96.5% 284,730 / 295,203
access_control ███████████████████░ 96.8% 849 / 877
customer_analytics ███████████████████░ 97.2% 7,482 / 7,700
data_catalog ███████████████████░ 97.4% 2,303 / 2,365
analytics_platform ████████████████████ 98.0% 2,102 / 2,145
metrics ████████████████████ 98.2% 2,404 / 2,448
pulse ████████████████████ 98.4% 2,017 / 2,049
live_debugger ████████████████████ 99.2% 613 / 618
field_notes ████████████████████ 99.4% 158 / 159

Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.

⚠️ MCP snapshots — 1 updated (1 modified, 0 added, 0 deleted)

Snapshots: MCP unit test snapshots updated

Changes: 1 snapshots (1 modified, 0 added, 0 deleted)

What this means:

  • Snapshots have been automatically updated to match current output

Next steps:

  • Review the changes to ensure they're intentional
  • If unexpected, investigate what caused the output to change

Review snapshot changes →

⚠️ Django migration SQL — 1 new migration to review

We've detected new migrations on this PR. Review the SQL output for each migration:

products/signals/backend/migrations/0063_alter_signalreportartefact_type.py

BEGIN;
--
-- Alter field type on signalreportartefact
--
-- (no-op)
COMMIT;

Last updated: 2026-07-17 17:04 UTC (8d6d7da)

Django migration risk — migration analysis complete

We've analyzed your migrations for potential risks.

Summary: 0 Safe | 1 Needs Review | 0 Blocked

⚠️ Needs Review

May have performance impact

signals.0063_alter_signalreportartefact_type
  └─ #1 ⚠️ AlterField
     Field alteration may cause table locks or data loss (check if changing type or constraints)
     model: signalreportartefact, field: type, field_type: CharField

Last updated: 2026-07-17 17:05 UTC (8d6d7da)

@Twixes
Twixes marked this pull request as ready for review July 17, 2026 14:18
@Twixes Twixes changed the title feat(signals): make resolved reports terminal, spawn fresh report on recurrence feat(signals): Spawn fresh report on recurrence Jul 17, 2026

@greptile-apps greptile-apps 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.

This review was skipped because it would exceed your organization's monthly flex usage limit. Raise the limit in billing settings or wait until the next billing period resets limits.

@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team July 17, 2026 14:19
Generated-By: PostHog Code
Task-Id: 5056349e-21fd-45da-a06b-026d62c95f20
@trunk-io

trunk-io Bot commented Jul 17, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

… model field

Replace the grouped_from_resolved_report FK with a symmetric related_report artefact
(recurrence_of / recurred_as) written on both reports. Keeps the recurrence linkage without a
schema-level model field, and generalizes to a reusable "related report" artefact type. The
research agent resolves the recurrence_of link to load the resolved report as context.

Generated-By: PostHog Code
Task-Id: 5056349e-21fd-45da-a06b-026d62c95f20
@github-actions
github-actions Bot requested a deployment to preview-pr-71981 July 17, 2026 16:17 In progress
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🦔 Hogbox preview · ✅ ready

▶ Open the preview

🔑 Login test@posthog.com / 12345678 (demo data)
🧩 Running this PR's backend and frontend, on the PostHog :master base
🔗 Link stable across rebuilds — a re-push swaps the box underneath, the URL stays
🔒 Access tailnet only (PostHog VPN)
🛠️ Admin inspect & debug state in hogland
💤 Idle sleeps after ~30 min idle (snapshot to S3, zero node cost) and wakes on your next visit in ~30s, behind a brief "waking up" screen

commit 8d6d7da · box box-0a69130562be · ready in 743s (push → usable) · build log · rebuilds on every push, torn down on close

Comment thread products/signals/backend/artefact_schemas.py Outdated
@github-actions
github-actions Bot requested a deployment to preview-pr-71981 July 17, 2026 16:22 In progress
"title_change": TitleChange,
"summary_change": SummaryChange,
"code_review": CodeReview,
"related_report": RelatedReport,

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.

Medium: Related-report links are writable by callers

Registering this type without adding it to NON_WRITABLE_ARTEFACT_TYPES lets any authenticated team member create or modify a recurrence_of link. _load_resolved_report_context trusts the first such link, so a caller can inject an arbitrary report's title and summary into research; because report_id only requires a nonempty string, a non-UUID value also raises during the UUID query and causes both activity attempts to fail. Treat these pipeline-generated links as read-only, validate the identifier as a UUID, and verify that the referenced report is resolved before using it.

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.

I don't think it's a security worry for this artefact type to be writable - it would make sense, for instance, for an agent to decide this and mark the connection via artefact creation tool. Sure, it's a surface for injecting content, but only for agents that already inject by design.

@veria-ai

veria-ai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

PR overview

This pull request adds support for spawning a fresh signal report when a recurrence is detected, including schema/model changes for linking the new report back to the original report context.

There are still two open security concerns around recurrence report linking. Authenticated team members can currently create or alter related-report links in ways that may inject arbitrary report context into downstream research or create cross-team report relationships, so the main remaining risk is improper trust and tenant scoping of these generated links. No issues have been addressed yet, so the PR still needs targeted validation and access-control fixes before merge.

Open issues (2)

Fixed/addressed: 0 · PR risk: 7/10

Per review, collapse the directional recurrence_of/recurred_as pair into one symmetric
related_to artefact (report_id only) written on both reports — created_at ordering captures
direction, so no relationship label is needed. The research reader identifies the recurrence
source as whichever linked report is resolved. Regenerated OpenAPI types accordingly.

Generated-By: PostHog Code
Task-Id: 5056349e-21fd-45da-a06b-026d62c95f20
@github-actions
github-actions Bot requested a deployment to preview-pr-71981 July 17, 2026 16:35 In progress
@hex-security-app

Copy link
Copy Markdown

Yes — Veria identified the same underlying trust-boundary issue. The current related_to artefact remains in the changed schema and is exposed through the generic artefact create API, while the research activity consumes linked resolved-report title/summary as agent context. It should be pipeline-only (non-writable to API callers); validating the UUID and retaining the resolved-status check are sensible defense-in-depth as well.

@Twixes
Twixes enabled auto-merge (squash) July 17, 2026 16:54
Move the symmetric back-link from a one-off grouping helper into SignalReportArtefact.add_log,
and route append()'s log types through add_log, so a related_to A→B always records B→A no matter
which write path creates it. The reverse row is written via _create to avoid recursion.

Generated-By: PostHog Code
Task-Id: 5056349e-21fd-45da-a06b-026d62c95f20
# row belongs to the same tenant.
cls._create(
team_id=team_id,
report_id=content.report_id,

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.

Medium: Cross-team report mutation

An authenticated user can link their report to a known report UUID from another team. This reverse insert copies the caller's team_id while pointing its foreign key at the other team's report, creating cross-tenant state and affecting unscoped consumers such as the report's artefact_count. Before either insert, resolve the target report with id=content.report_id, team_id=team_id and reject foreign or deleted reports; perform both inserts in one transaction.

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.

@cursoragent Open a new PR for this

@Twixes
Twixes merged commit 1de7ec9 into master Jul 17, 2026
274 checks passed
@Twixes
Twixes deleted the posthog-code/signals-resolved-terminal branch July 17, 2026 17:26
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 17, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-17 17:52 UTC Run
prod-us ✅ Deployed 2026-07-17 18:17 UTC Run
prod-eu ✅ Deployed 2026-07-17 18:16 UTC Run

andrewm4894 added a commit that referenced this pull request Jul 20, 2026
Addresses review findings on the user-driven resolve API.

Four bot findings argued that resolve-then-refund reopens "repeatable free
work" because resolved reports re-promote to candidate on new signals. They
don't: RESOLVED -> CANDIDATE is absent from transition_to, and a signal
matching a resolved report spawns a fresh report instead. ARCHITECTURE.md
still documented the old behaviour (resolved -> candidate, RESOLVED
re-promotion under the re-research cap), which #71981 removed — corrected,
along with the code comments that repeated the claim.

The defensible version of that concern was real and is now fixed: the refund
guard keyed on SUPPRESSED, but a refunded merged-PR report deliberately stays
RESOLVED, so it could be snoozed to POTENTIAL and re-enter research. The guard
now refuses any refunded report going back to POTENTIAL, whatever its current
status.

Also:
- Resolve out of the archive now requires the report to have been researched
  before it was archived, so candidate -> suppressed -> resolved can't land a
  report in RESOLVED with no title or summary.
- Resolve attribution is resolved once per request instead of once per report,
  collapsing bulk_state's repeated X-PostHog-Task-Id lookup into one query.
- output.pr_merged is now webhook-only: set_task_run_output no longer accepts
  it from callers, since signals reads it to decide refund finality and it was
  writable with a plain task:write token.
- Reports expose implementation_pr_merged, so the inbox PR badge and the
  refund navigation read the real merge state instead of inferring "merged"
  from RESOLVED — which a direct resolve makes wrong.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants