feat(signals): add scout lifecycle status with reason-scoped pauses - #75349
Conversation
|
😎 Merged manually by @andrewm4894 - details. |
👀 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 |
🦔 Hogbox preview · ✅ ready▶ Open the preview
commit |
Prompt To Fix All With AI### Issue 1
products/signals/backend/migrations/0076_scout_config_status_constraint.py:25-28
**Constraint breaks rolling writes**
When migration 0076 validates this constraint before all old web instances have been replaced, those instances still write `enabled` without updating `status`, causing PostgreSQL to reject otherwise valid scout enable or disable requests with a check-constraint violation.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "feat(signals): add scout lifecycle statu..." | Re-trigger Greptile |
🤖 CI report✅ Bundle size — no changeUncompressed size of every built Total: 65.34 MiB · no change No file changed by more than 1000 B. Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report ✅ Eager graph — within budgetHow 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
🟢 Largest files eagerly shipped from
|
| 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.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 |
| 226.9 KiB | ../node_modules/.pnpm/posthog-js@1.409.0/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.2% of 5.72 MiB |
| Deferred (lazy) | 2.07 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-NOGOCMN4.css |
| 546.5 KiB | dist/toolbar/chunk-chunk-QEEBTNQU.js |
| 484.4 KiB | dist/toolbar/chunk-chunk-QE3IUXXW.js |
| 133.6 KiB | dist/toolbar/chunk-chunk-SITDALNN.js |
| 131.8 KiB | dist/toolbar/chunk-chunk-T5KY5WYR.js |
| 71.0 KiB | dist/toolbar/toolbar-app-UA34RKOZ.js |
| 69.0 KiB | dist/toolbar/chunk-chunk-27JL52RE.js |
| 35.6 KiB | dist/toolbar/chunk-chunk-XJSP6AE6.js |
| 20.9 KiB | dist/toolbar/chunk-chunk-RX2K3TFD.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 — 🔺 +2.5 KiB (+0.0%)
Total size of the built frontend/dist folder (all assets), compared against the base branch.
Total: 1370.30 MiB · 🔺 +2.5 KiB (+0.0%)
⚠️ Playwright — 1 flaky
🎭 Playwright report · View test results →
- Split a person with multiple distinct IDs (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 snapshots — 13 updated (13 modified, 0 added, 0 deleted)
Query snapshots: Backend query snapshots updated
Changes: 13 snapshots (13 modified, 0 added, 0 deleted)
What this means:
- Query snapshots have been automatically updated to match current output
- These changes reflect modifications to database queries or schema
Next steps:
- Review the query changes to ensure they're intentional
- If unexpected, investigate what caused the query to change
⚠️ Backend coverage — 98.0% of changed backend lines covered — 4 uncovered
🧪 Backend test coverage
Patch coverage — changed backend lines (products + core): ████████████████████ 98.0% (277 / 281)
| File | Patch | Uncovered changed lines |
|---|---|---|
products/signals/backend/admin.py |
0.0% | 102, 110 |
products/signals/backend/migrations/0078_backfill_scout_status.py |
71.4% | 9, 12 |
🤖 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 30586088991 -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.2% |
8,629 / 22,000 |
demo |
███████████░░░░░░░░░ 56.3% |
1,497 / 2,661 |
warehouse_sources_queue |
████████████░░░░░░░░ 59.2% |
148 / 250 |
data_tools |
██████████████░░░░░░ 70.0% |
63 / 90 |
tasks |
██████████████░░░░░░ 70.0% |
32,688 / 46,675 |
ai_gateway |
███████████████░░░░░ 75.0% |
9 / 12 |
signals |
████████████████░░░░ 81.3% |
24,592 / 30,261 |
cdp |
████████████████░░░░ 82.1% |
3,285 / 3,999 |
data_modeling |
█████████████████░░░ 85.6% |
7,668 / 8,959 |
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 |
mcp_analytics |
██████████████████░░ 90.5% |
3,041 / 3,361 |
links |
██████████████████░░ 90.6% |
183 / 202 |
streamlit_apps |
██████████████████░░ 90.7% |
2,630 / 2,901 |
error_tracking |
██████████████████░░ 91.0% |
10,925 / 12,004 |
slack_app |
██████████████████░░ 91.1% |
9,632 / 10,576 |
stamphog |
██████████████████░░ 91.1% |
4,056 / 4,450 |
marketing_analytics |
██████████████████░░ 91.2% |
12,092 / 13,265 |
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 |
███████████████████░ 93.9% |
6,922 / 7,375 |
endpoints |
███████████████████░ 94.2% |
8,655 / 9,192 |
tracing |
███████████████████░ 94.5% |
2,670 / 2,826 |
review_hog |
███████████████████░ 94.6% |
7,979 / 8,437 |
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,435 / 10,928 |
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,474 / 18,130 |
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% |
358,410 / 368,477 |
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.
⚠️ MCP snapshots — 2 updated (2 modified, 0 added, 0 deleted)
Snapshots: MCP unit test snapshots updated
Changes: 2 snapshots (2 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
⚠️ Django migration SQL — 2 new migrations to review
We've detected new migrations on this PR. Review the SQL output for each migration:
products/signals/backend/migrations/0077_signalscoutconfig_status.py
BEGIN;
--
-- Add field status to signalscoutconfig
--
ALTER TABLE "signals_signalscoutconfig" ADD COLUMN "status" varchar(20) DEFAULT 'active' NOT NULL;
--
-- Add field pause_reason to signalscoutconfig
--
ALTER TABLE "signals_signalscoutconfig" ADD COLUMN "pause_reason" varchar(20) NULL;
--
-- Add field status_changed_at to signalscoutconfig
--
ALTER TABLE "signals_signalscoutconfig" ADD COLUMN "status_changed_at" timestamp with time zone NULL;
--
-- Add field status_changed_by to signalscoutconfig
--
ALTER TABLE "signals_signalscoutconfig" ADD COLUMN "status_changed_by_id" integer NULL;
COMMIT;products/signals/backend/migrations/0078_backfill_scout_status.py
BEGIN;
--
-- Raw Python operation
--
-- THIS OPERATION CANNOT BE WRITTEN AS SQL
COMMIT;Last updated: 2026-07-30 22:12 UTC (2949a69)
✅ Django migration risk — migration analysis complete
We've analyzed your migrations for potential risks.
Summary: 1 Safe | 1 Needs Review | 0 Blocked
⚠️ Needs Review
May have performance impact
signals.0078_backfill_scout_status
└─ #1 ⚠️ RunPython: RunPython data migration needs review for performance
✅ Safe
Brief or no lock, backwards compatible
signals.0077_signalscoutconfig_status
└─ #1 ✅ AddField
Adding NOT NULL field with constant default (safe in PG11+)
model: signalscoutconfig, field: status
└─ #2 ✅ AddField
Adding nullable field requires brief lock
model: signalscoutconfig, field: pause_reason
└─ #3 ✅ AddField
Adding nullable field requires brief lock
model: signalscoutconfig, field: status_changed_at
└─ #4 ✅ AddField
Adding nullable field requires brief lock
model: signalscoutconfig, field: status_changed_by
📚 How to Deploy These Changes Safely
AddField:
This operation acquires a brief lock but doesn't rewrite the table.
Deployment uses lock timeouts with automatic retries, so lock contention will cause retries rather than connection pile-up.
RunPython:
Use batching for large data migrations:
- Use
.iterator()to avoid loading all rows into memory - Use
.bulk_update()instead of saving individual objects - Batch size: 1,000-10,000 rows per batch
- Add pauses between batches
- Consider background jobs for very large updates (millions of rows)
See the migration safety guide
Last updated: 2026-07-30 22:12 UTC (2949a69)
🦔 ReviewHog reviewed this pull requestFound 1 must fix, 1 should fix, 0 consider. Published 2 findings (view the review). |
|
ReviewHog Alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏 |
There was a problem hiding this comment.
ReviewHog Report
Business logic
Issues: 1 issue
Files (4)
products/signals/backend/migrations/0075_signalscoutconfig_status.pyproducts/signals/backend/models.pyproducts/signals/backend/admin.pyposthog/models/activity_logging/activity_log.py
What were the main changes
- Adds status/pause_reason/status_changed_at/status_changed_by columns to SignalScoutConfig via migration, with enabled=False backfilled to paused_by_user
- Introduces Status/PauseReason enums and the reason-scoped ownership rule enforced by transition_status_by_system() (now row-locked with select_for_update per review feedback)
- save() reconciles the enabled/status pair for legacy writers that only touch one side; a DB CHECK constraint is deliberately deferred to a follow-up migration to avoid breaking rolling deploys
- Adds in_cold_start_grace() helper, re-anchored only on human re-enable (status_changed_by non-null) to avoid a sweep re-entering its own grace window
- Admin exposes the new status cluster but makes it read-only (avoids loading the full user table for status_changed_by) and adds status to list_display/list_filter
- Activity log config adds a pause_reason label and excludes status_changed_at/status_changed_by as bookkeeping
Api
Issues: 1 issue
Files (6)
products/signals/backend/scout_harness/serializers.pyposthog/settings/web.pyproducts/signals/frontend/generated/api.schemas.tsproducts/signals/frontend/generated/api.zod.tsservices/mcp/src/api/generated.tsservices/mcp/src/generated/signals/api.ts
What were the main changes
- Exposes status and pause_reason as read-only fields on SignalScoutConfigSerializer, with updated help text clarifying enabled is derived from status
- Update serializer now routes enabled writes through status transitions (paused_by_user / active) and clears a pending_pause on any human config edit (guarded against empty PATCH payloads per review fix)
- Registers ScoutConfigStatusEnum / ScoutConfigPauseReasonEnum in ENUM_NAME_OVERRIDES to avoid an enum-name collision with the bare
statusfield - Regenerates frontend and MCP API types/zod schemas to reflect the new enums and updated field descriptions
aec3a5a to
c7dd794
Compare
master landed 0075_alter_signalsourceconfig_source_product while this branch was open, so the scout status migrations move to 0076/0077 and now depend on it. max_migration.txt points at the backfill again. Generated-By: PostHog Code Task-Id: ca5a1aa6-8fb7-4388-a756-42e40cbc41aa
|
/trunk merge |
# Conflicts: # products/signals/backend/migrations/max_migration.txt
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ec822dd4e6
ℹ️ 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".
| not_running_reason = "auto_paused" | ||
| pause_reason = config.pause_reason |
There was a problem hiding this comment.
Teach the fleet prompt about automatic pauses
When a scout enters paused_by_system, this builder now emits not_running_reason="auto_paused", but the runtime _FLEET_SEAMS instructions in prompt.py:187 still tell every scout that this field means only that someone turned the sibling off or its skill is unavailable; ScoutFleetSerializer.disabled at lines 1227-1232 repeats the same incomplete contract. The fleet can therefore misinterpret an automatic lifecycle decision as operator intent despite the new value being introduced specifically to preserve that distinction. Fresh evidence after the prior profile finding was marked fixed is that these two consumers remain unchanged; update them to explain auto_paused and its pause_reason.
AGENTS.md reference: products/signals/backend/scout_harness/AGENTS.md:L13-L14
Useful? React with 👍 / 👎.
Reworked onto the scout lifecycle status primitive (#75349): the failure-streak breaker pauses via transition_status_by_system (paused_by_system + repeated_failures, syncing enabled=false), and the half-open probe is a reason-scoped coordinator rule cooled down on last_run_at. Also folds in the deferred enabled<->status reconciliation and the NOT VALID + VALIDATE coherence constraints. Claude-Session: https://claude.ai/code/session_01QXwLjesj6xXp7kwPjnFNFk
Problem
A scout's whole lifecycle today is
SignalScoutConfig.enabled, a single bit that conflates two different facts: a human turned this off, and the system turned this off.That distinction is about to matter, because two independent auto-pause mechanisms are in flight at the same time. #75053 pauses scouts that produce nothing and #74458 pauses scouts that keep failing, and each bolts its own
auto_paused_at/auto_pause_reasonside-fields ontoenabledwith conflicting semantics (one flipsenabledand stays off, the other never touchesenabledand auto-resumes). They also both claim migration 0075. Without a shared primitive they become two parallel pause mechanisms that can fight over the same flag, re-pause what a human re-enabled, and clear each other's state.This PR ships that shared primitive, and nothing else. Related: #74866.
Changes
Zero behavior change. No sweep, no breaker, nothing pauses anything. The coordinator, API, MCP tools, and frontend behave exactly as before.
flowchart LR A[active] -->|system: warn| PP[pending_pause] PP -->|system: pause| PS[paused_by_system] A -->|system: pause| PS PS -->|"human enabled=true, or system resume (same reason only)"| A PP -->|any human config edit| A A -->|"human enabled=false"| PU[paused_by_user] PS -->|"human enabled=false"| PU PU -->|"human enabled=true"| A classDef phBlue fill:#1d4aff,stroke:#1d4aff,color:#fff; classDef phRed fill:#f54e00,stroke:#f54e00,color:#fff; classDef phYellow fill:#f9bd2b,stroke:#f9bd2b,color:#000; class A phBlue; class PP phYellow; class PS,PU phRed;statusenum onSignalScoutConfig:active/pending_pause/paused_by_system/paused_by_user, plus a separatepause_reason(no_output/ignored/repeated_failures) and astatus_changed_attimestamp. Who paused the scout is the state itself, not a side-channel field, because the two pauses must behave differently: the system may resume its own pauses but must never touch a human's.transition_status_by_system(), the one entry point for system writers: a writer may only set or clear pauses carrying its ownpause_reason, andpaused_by_useris untouchable by any system writer. This is how feat(signals): auto-pause scouts that produce nothing #75053 and feat(signals): auto-pause scouts that keep failing, and name the wall #74458 coexist once they rebase onto this: the failure breaker's probe can resume arepeated_failurespause but can never resurrect ano_outputone, and neither can undo a human's decision.enabledstays a real, dual-written column. It is filtered at SQL level in the coordinator and mirrored to the warehouse, so it cannot become a property.statusis the source of truth andenabled = status in (active, pending_pause).save()reconciles the pair for writers that only set one side. A DB CHECK constraint backstop is deliberately deferred to a follow-up migration: enforcing it in the deploy that introduces the dual-write would break rolling deploys, since not-yet-replaced instances still writeenabledalone (review catch by Greptile and Codex).enabled:PATCH enabled=falserecordspaused_by_user,PATCH enabled=trueis the one-click resume from any pause. Any config edit clears apending_pause, since a human tending the config is exactly the signal the warning exists to detect.status_changed_byrecords the human behind the last status change (stamped from the config API), and is null for system transitions, so a human re-enable and a system resume are distinguishable and a system pause never reads as a person's action. Who enabled stays onenabled_by; who last edited anything else stays the activity log's job. Stored but deliberately not exposed on the config serializer yet: the config list is readable inside scout sandboxes, where member identity is gated, so exposing it deserves its own decision alongside the UI that needs it. The FK isdb_constraint=Falsebecauseposthog_useris a hot table.in_cold_start_grace()(14 days from creation or the last human re-enable) for future sweeps to skip provisional scouts. Deliberately not persisted as anewstatus: a one-shotnew -> activetransition that ever failed to fire would leave a scout permanently exempt from every system writer.enabled=falserows becomepaused_by_user, which is the only thing they can have been.statusandpause_reasonare exposed read-only on the config serializers and flow into generated types and MCP tool schemas. Status changes are activity-logged;status_changed_atis excluded as bookkeeping.Note
#75053 and #74458 should rebase onto this as consumers rather than land their own
auto_pause_*field sets: the inactivity sweep writespending_pause->paused_by_system+no_output, the failure breaker writespaused_by_system+repeated_failureswith its probe expressed as a reason-scoped coordinator rule.How did you test this code?
New
products/signals/backend/test/test_scout_status.py(18 tests), each catching a regression nothing else did:save()reconciliation: anenabled-only write (fixtures, scripts) leavingstatusstale, and astatus-only write leavingenabledstale.Three new endpoint tests in
test_scout_harness_api.pyas wiring guards: disable recordspaused_by_user, enable resumes a system pause and clears its reason, and a schedule-only edit clearspending_pause(that mapping lives only in the update serializer, so no model test covers it).Also ran locally: the full signals backend suite (1,887 passed), repo-wide
uv run mypy --cache-fine-grained .(clean),hogli build:openapi(generated types committed), MCP unit tests with two snapshot updates, andhogli ci:preflight --fix(0 failures).sqlmigrateverified theADD COLUMNkeeps its Postgres-level default.Automatic notifications
Docs update
No user-facing docs describe scout pausing yet; the docs change belongs with the consumer PRs that make pausing actually happen.
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Written by Claude (Fable) in Claude Code, directed by @andrewm4894, who reviewed the design before any code: the enum shape, the separate reason field, and the ownership rule were agreed first, and this PR was deliberately scoped to the primitive alone so the two in-flight auto-pause PRs land inside the frame rather than defining it.
Skills invoked:
/django-migrations,/improving-drf-endpoints,/writing-tests,/writing-code-comments,/writing-user-facing-copy.Decisions worth flagging:
save()reconciliation over editing everyenabledwriter: test fixtures and ad-hoc scripts create configs withenabled=Falsedirectly, so the model keeps the pair consistent rather than every caller learning aboutstatus.is_newserializer field was written and then removed on Andy's call: nothing renders it yet,created_atis already exposed for a client-side calculation, and the sweep uses the model helper server-side. It ships with the first badge that needs it.status, notscout_status, because the warehouse column of that name uses a different value set (new/active/inactive/stale) and reusing the name would blur two vocabularies.ScoutConfigStatusEnum/ScoutConfigPauseReasonEnumwere registered inENUM_NAME_OVERRIDESup front, since a barestatusChoiceField is the classic enum-name collision.