feat(signals): auto-pause scouts that produce nothing - #75053
feat(signals): auto-pause scouts that produce nothing#75053posthog[bot] wants to merge 2 commits into
Conversation
|
✨ Submitted to Merge by Andy Maguire (a GitHub user). It will be added to the merge queue once all branch protection rules pass and there are no merge conflicts with the target branch. See more details here. |
🤖 CI report
|
| File | Size | Δ vs base |
|---|---|---|
posthog-app/src/scenes/inbox/InboxScene.js |
713.8 KiB | 🔺 +1.5 KiB (+0.2%) |
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.25 MiB · 22 files | no change | ███░░░░░░░ 27.7% of 4.51 MiB |
authenticated shell (every logged-in page)src/scenes/AuthenticatedShell.tsx |
8.12 MiB · 3,023 files | no change | ████████░░ 83.6% of 9.71 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 |
|---|---|
| 285.3 KiB | ../node_modules/.pnpm/posthog-js@1.409.2/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 |
| 231.4 KiB | ../node_modules/.pnpm/posthog-js@1.409.2/node_modules/posthog-js/dist/module.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.2 KiB | src/lib/api.ts |
| 94.7 KiB | ../packages/quill/packages/quill/dist/index.js |
| 93.3 KiB | ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js |
| 90.6 KiB | ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/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
✅ Toolbar bundle — eager 2.19 MiB within budget
What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.
| Metric | Size | Δ vs base | Budget |
|---|---|---|---|
| Eager (shipped) entry + static imports |
2.19 MiB · 17 files | no change | ████░░░░░░ 38.3% of 5.72 MiB |
| Deferred (lazy) | 2.08 MiB · 33 files | no change | n/a — loads on demand |
Loader dist/toolbar.js |
1.1 KiB | no change | █░░░░░░░░░ 5.8% of 19.5 KiB |
Largest eagerly-shipped chunks
| Size | File |
|---|---|
| 717.7 KiB | dist/toolbar/toolbar-app-3ZUOLZ25.css |
| 551.1 KiB | dist/toolbar/chunk-chunk-HCVXQK4K.js |
| 484.6 KiB | dist/toolbar/chunk-chunk-I33HJPDC.js |
| 133.6 KiB | dist/toolbar/chunk-chunk-LD5GQHAS.js |
| 131.8 KiB | dist/toolbar/chunk-chunk-T5KY5WYR.js |
| 71.0 KiB | dist/toolbar/toolbar-app-DUPY5K74.js |
| 69.0 KiB | dist/toolbar/chunk-chunk-27JL52RE.js |
| 35.6 KiB | dist/toolbar/chunk-chunk-7YZYKQFX.js |
| 20.9 KiB | dist/toolbar/chunk-chunk-4TTVMURW.js |
| 12.2 KiB | dist/toolbar/chunk-chunk-PIK3PADE.js |
Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile
✅ Dist folder size — 🔺 +6.4 KiB (+0.0%)
Total size of the built frontend/dist folder (all assets), compared against the base branch.
Total: 1371.16 MiB · 🔺 +6.4 KiB (+0.0%)
⚠️ Backend coverage — 94.0% of changed backend lines covered — 20 uncovered
🧪 Backend test coverage
Patch coverage — changed backend lines (products + core): ███████████████████░ 94.0% (321 / 341)
| File | Patch | Uncovered changed lines |
|---|---|---|
products/signals/backend/tasks.py |
23.5% | 518–519, 527–528, 531–532, 536–537, 541–545 |
products/signals/backend/scout_harness/serializers.py |
92.0% | 2077–2078 |
products/signals/backend/scout_harness/inactivity.py |
95.0% | 153, 155–156, 225, 286 |
🤖 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 30630511121 -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.4% |
8,782 / 22,290 |
demo |
███████████░░░░░░░░░ 56.3% |
1,497 / 2,661 |
warehouse_sources_queue |
████████████░░░░░░░░ 59.2% |
148 / 250 |
data_tools |
██████████████░░░░░░ 70.0% |
63 / 90 |
tasks |
██████████████░░░░░░ 70.1% |
32,865 / 46,867 |
ai_gateway |
███████████████░░░░░ 75.0% |
9 / 12 |
signals |
████████████████░░░░ 81.4% |
25,264 / 31,030 |
cdp |
████████████████░░░░ 82.1% |
3,285 / 3,999 |
data_modeling |
█████████████████░░░ 85.7% |
7,716 / 9,008 |
notebooks |
█████████████████░░░ 86.0% |
7,794 / 9,060 |
wizard |
█████████████████░░░ 86.4% |
1,060 / 1,227 |
actions |
█████████████████░░░ 86.6% |
717 / 828 |
cohorts |
██████████████████░░ 87.8% |
6,181 / 7,040 |
product_tours |
██████████████████░░ 87.9% |
1,303 / 1,482 |
exports |
██████████████████░░ 88.2% |
7,046 / 7,986 |
data_warehouse |
██████████████████░░ 88.4% |
12,192 / 13,798 |
business_knowledge |
██████████████████░░ 89.0% |
4,391 / 4,936 |
engineering_analytics |
██████████████████░░ 89.3% |
6,529 / 7,309 |
conversations |
██████████████████░░ 89.3% |
17,600 / 19,701 |
dashboards |
██████████████████░░ 89.4% |
5,983 / 6,693 |
visual_review |
██████████████████░░ 89.5% |
5,870 / 6,558 |
alerts |
██████████████████░░ 90.2% |
4,458 / 4,942 |
links |
██████████████████░░ 90.6% |
183 / 202 |
streamlit_apps |
██████████████████░░ 90.7% |
2,630 / 2,901 |
mcp_analytics |
██████████████████░░ 90.9% |
3,191 / 3,511 |
error_tracking |
██████████████████░░ 91.0% |
10,926 / 12,008 |
slack_app |
██████████████████░░ 91.1% |
9,664 / 10,610 |
marketing_analytics |
██████████████████░░ 91.2% |
12,092 / 13,265 |
stamphog |
██████████████████░░ 91.3% |
4,505 / 4,936 |
mcp_store |
██████████████████░░ 91.9% |
4,257 / 4,634 |
product_analytics |
███████████████████░ 92.5% |
5,849 / 6,321 |
managed_migrations |
███████████████████░ 92.6% |
1,556 / 1,681 |
early_access_features |
███████████████████░ 92.6% |
1,287 / 1,390 |
notifications |
███████████████████░ 92.6% |
1,017 / 1,098 |
ai_observability |
███████████████████░ 92.8% |
15,618 / 16,821 |
surveys |
███████████████████░ 93.1% |
5,771 / 6,197 |
posthog_ai |
███████████████████░ 93.2% |
1,326 / 1,422 |
web_analytics |
███████████████████░ 93.3% |
14,911 / 15,976 |
approvals |
███████████████████░ 93.3% |
3,437 / 3,682 |
reminders |
███████████████████░ 93.4% |
468 / 501 |
legal_documents |
███████████████████░ 93.8% |
1,628 / 1,736 |
workflows |
███████████████████░ 94.1% |
7,241 / 7,698 |
endpoints |
███████████████████░ 94.2% |
8,655 / 9,192 |
tracing |
███████████████████░ 94.5% |
2,671 / 2,827 |
review_hog |
███████████████████░ 94.6% |
8,101 / 8,563 |
skills |
███████████████████░ 94.6% |
3,158 / 3,337 |
messaging |
███████████████████░ 94.7% |
2,885 / 3,048 |
experiments |
███████████████████░ 95.4% |
25,938 / 27,177 |
logs |
███████████████████░ 95.5% |
10,440 / 10,937 |
growth |
███████████████████░ 96.1% |
3,245 / 3,376 |
annotations |
███████████████████░ 96.2% |
732 / 761 |
revenue_analytics |
███████████████████░ 96.3% |
1,887 / 1,960 |
feature_flags |
███████████████████░ 96.4% |
17,488 / 18,144 |
replay_vision |
███████████████████░ 96.4% |
16,081 / 16,679 |
user_interviews |
███████████████████░ 96.5% |
2,638 / 2,734 |
access_control |
███████████████████░ 96.9% |
870 / 898 |
customer_analytics |
███████████████████░ 97.1% |
10,444 / 10,758 |
warehouse_sources |
███████████████████░ 97.3% |
359,280 / 369,280 |
data_catalog |
████████████████████ 97.7% |
2,588 / 2,648 |
analytics_platform |
████████████████████ 98.0% |
2,153 / 2,197 |
metrics |
████████████████████ 98.2% |
2,491 / 2,536 |
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.
⚠️ 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/0083_signalscoutconfig_auto_pause_exempt.py
BEGIN;
--
-- Add field auto_pause_exempt to signalscoutconfig
--
ALTER TABLE "signals_signalscoutconfig" ADD COLUMN "auto_pause_exempt" boolean DEFAULT false NOT NULL;
COMMIT;Last updated: 2026-07-31 12:29 UTC (8834cce)
✅ Django migration risk — migration analysis complete
We've analyzed your migrations for potential risks.
Summary: 1 Safe | 0 Needs Review | 0 Blocked
✅ Safe
Brief or no lock, backwards compatible
signals.0083_signalscoutconfig_auto_pause_exempt
└─ #1 ✅ AddField
Adding NOT NULL field with constant default (safe in PG11+)
model: signalscoutconfig, field: auto_pause_exempt
Last updated: 2026-07-31 12:29 UTC (8834cce)
👀 Auto-assigned reviewersThese soft owners were skipped because they only have minor changes here. Nothing blocks merge, so self-assign if you'd like a look:
Soft owners come from each directory's |
|
Heads-up on direction: the lifecycle primitive this PR needs has now landed separately in #75349 (approved), and the plan agreed in the linked design discussion is for this PR to rebase onto it as a consumer rather than introduce its own pause fields. What #75349 gives you, replacing the
Scope requests for the rebase, from the same design discussion:
Mechanical notes: migrations |
Daily sweep that warns, then pauses scouts with no output and no human engagement over the window, built on the scout lifecycle status primitive: warn is pending_pause, pause is paused_by_system, both via transition_status_by_system with the sweep-owned reasons no_output and ignored. Adds auto_pause_exempt (opt-out, auto-set on human re-enable of a sweep pause), a per-sweep cap on new warnings, and a revert analytics event as the false-positive metric.
076923f to
78c1957
Compare
🦔 ReviewHog is reviewing this pull requestStep 5/6 · Validating findings · 3/14 Specialist review skills read the changed code in parallel each from their own perspective, a blind-spot sweep catches what they missed, and only validated findings are published back to this pull request. This comment updates as the review progresses. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 78c1957a28
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
/trunk merge |
Problem
A scout that produces nothing anyone uses keeps running a sandboxed agent on its cadence forever, because
enabledonly ever moves by hand. On a strict reading of "no output and no engagement over two weeks", most of the running fleet qualifies, so the wasted lanes dominate fleet spend. Pausing them is what frees budget for the teams that actually engage.Closes #74866
Changes
This PR was reworked in place onto the scout lifecycle status primitive from #75349, as flagged in the earlier coordination comment. The failure breaker (#74458, merged) is the sibling consumer on the failure axis; this is the waste axis.
flowchart TD A{{"Daily sweep"}} --> B["Candidates: enabled, emitting, not exempt, past cold-start grace"] B --> C{"Productive in the last 14 days?"} C -- "yes" --> D["Clear any pending warning back to active"] C -- "no" --> E{"Ran enough times in the window?"} E -- "no" --> F["Left alone"] E -- "yes" --> G{"Already warned?"} G -- "no" --> H["Warn: pending_pause, reason no_output or ignored. Capped per sweep"] G -- "yes, a week ago" --> I["Pause: paused_by_system, enabled false"] classDef phBlue fill:#1d4aff,stroke:#1d4aff,color:#fff; classDef phYellow fill:#f9bd2b,stroke:#f9bd2b,color:#000; classDef phGray fill:#e5e7eb,stroke:#c7ccd1,color:#000; class A phYellow; class H,I phBlue; class D,F phGray;pause_inactive_signal_scouts, 06:15 UTC) drivesscout_harness/inactivity.py. A scout counts as productive if any run in the last 14 days recorded output on any of the three emit channels (emitted_finding_ids/emitted_report_ids/edited_report_ids), or a person engaged with a report it wrote earlier (a log artefact attributed to a user, or the report reaching a user-driven status). Pipeline-written artefacts do not count.status=pending_pause(still scheduled), the pause isstatus=paused_by_system(syncingenabled=false), both throughtransition_status_by_systemwith the sweep-owned reasonsno_output(surfaced nothing) andignored(surfaced reports nobody picked up). The helper's reason-scoped ownership rule keeps this sweep and the failure breaker from touching each other's pauses, andevaluated_atmakes a racing human edit win over a sweep decision made on stale reads.in_cold_start_grace()(re-anchored by a resume), a minimum-runs floor so budget-starved or slow-cadence scouts are never judged on absence of opportunity, dry-run scouts skipped, and a per-sweep cap on new warnings. Capping warnings bounds later pauses too, since a pause can only follow a warning by the seven-day grace, and it keeps every warned scout's "pauses in a week" promise honest. Deferrals are counted and logged so a capped sweep reads as partial, not clean.auto_pause_exempton the config (migration 0083, non-blocking AddField withdb_default): the opt-out for watchdog scouts whose whole job is staying quiet. It is also set automatically when a human re-enables a sweep-paused scout, so the sweep never overrules a person twice. An explicitauto_pause_exempt=falsein the same re-enable request wins.signals_scout_auto_pause_warned/signals_scout_auto_paused, and a human re-enable of a sweep pause emitssignals_scout_auto_pause_revertedwith hours-since-pause, the leading false-positive indicator (a revert within a day means the rule paused something someone still wanted). Activity log entries for sweep transitions carry the job trigger so they never read as unattributed human edits.pending_pauseand "Paused" once paused (folded into one lifecycle badge with the breaker's), and the config form gains a "Keep running while quiet" switch.status_changed_atandauto_pause_exemptare exposed read-only on the config serializer; generated types and MCP tool schemas regenerated.scout_harness/AGENTS.mdand theauthoring-scoutsskill.Note
The pause is one-click reversible (
enabled=true), and that re-enable permanently exempts the scout from this sweep. There is no probe on this axis: unlike arepeated_failurespause, an inactivity pause never runs again on its own.How did you test this code?
Automated only, all run locally by the agent:
test_scout_inactivity.py(21 tests): warn-then-pause sequencing and theno_outputvsignoredclassification; each emit channel and each human-engagement shape keeping a scout alive (pipeline artefacts don't); recovery clearing a warning; exempt / dry-run / user-paused / breaker-paused rows untouched; cold start; the minimum-runs floor; a resumed scout getting a full fresh window; the per-sweep warn cap deferring the overflow; the pause surviving lazy-seed reconciliation; sweep transitions activity-logged with the job trigger and no user.falsein the same request wins), emits the revert event, a breaker pause does not mark exempt, and exempting clears a pending warning.makemigrations --checkclean,sqlmigrateconfirms a single non-blockingADD COLUMN ... DEFAULT false NOT NULL.Automatic notifications
Docs update
scout_harness/AGENTS.mdandproducts/signals/skills/authoring-scouts/SKILL.mdupdated in this PR.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Reworked in a Claude Code session directed by the assignee, replacing the original bot-authored implementation in place (force-push, single commit). Skills invoked: /django-migrations, /writing-tests, /writing-user-facing-copy, /merging-prs.
Decisions along the way:
auto_pause_*columns for the feat(signals): add scout lifecycle status with reason-scoped pauses #75349 primitive; onlyauto_pause_exemptsurvives as a column. The warning timestamp is now juststatus_changed_aton apending_pauserow, and the "fresh window on resume" stamp is the grace helper's re-anchor.ignoredback: scouts whose output nobody acts on are the point of the sweep, and a fleet quietly shrinking to its engaged core is the intended outcome, not a failure mode.