Skip to content

feat(signals): add scout lifecycle status with reason-scoped pauses - #75349

Merged
andrewm4894 merged 14 commits into
masterfrom
feat/scout-status-enum
Jul 30, 2026
Merged

feat(signals): add scout lifecycle status with reason-scoped pauses#75349
andrewm4894 merged 14 commits into
masterfrom
feat/scout-status-enum

Conversation

@andrewm4894

@andrewm4894 andrewm4894 commented Jul 30, 2026

Copy link
Copy Markdown
Member

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_reason side-fields onto enabled with conflicting semantics (one flips enabled and stays off, the other never touches enabled and 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;
Loading
  • status enum on SignalScoutConfig: active / pending_pause / paused_by_system / paused_by_user, plus a separate pause_reason (no_output / ignored / repeated_failures) and a status_changed_at timestamp. 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.
  • The reason-scoped ownership rule, enforced by transition_status_by_system(), the one entry point for system writers: a writer may only set or clear pauses carrying its own pause_reason, and paused_by_user is 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 a repeated_failures pause but can never resurrect a no_output one, and neither can undo a human's decision.
  • enabled stays 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. status is the source of truth and enabled = 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 write enabled alone (review catch by Greptile and Codex).
  • Human writes keep flowing through enabled: PATCH enabled=false records paused_by_user, PATCH enabled=true is the one-click resume from any pause. Any config edit clears a pending_pause, since a human tending the config is exactly the signal the warning exists to detect.
  • Who moved it: status_changed_by records 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 on enabled_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 is db_constraint=False because posthog_user is a hot table.
  • Cold-start grace as a helper, not a state: 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 a new status: a one-shot new -> active transition that ever failed to fire would leave a scout permanently exempt from every system writer.
  • Backfill: existing enabled=false rows become paused_by_user, which is the only thing they can have been.
  • status and pause_reason are exposed read-only on the config serializers and flow into generated types and MCP tool schemas. Status changes are activity-logged; status_changed_at is 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 writes pending_pause -> paused_by_system + no_output, the failure breaker writes paused_by_system + repeated_failures with 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:

  • The ownership-rule matrix (parameterized): a system writer overwriting or resuming another writer's pause, or touching a human pause, would previously have been possible with raw field writes and would break the two-consumer design silently.
  • save() reconciliation: an enabled-only write (fixtures, scripts) leaving status stale, and a status-only write leaving enabled stale.
  • Cold-start grace boundaries, including that a human re-enable grants a fresh window and a system pause does not.

Three new endpoint tests in test_scout_harness_api.py as wiring guards: disable records paused_by_user, enable resumes a system pause and clears its reason, and a schedule-only edit clears pending_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, and hogli ci:preflight --fix (0 failures). sqlmigrate verified the ADD COLUMN keeps its Postgres-level default.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

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 every enabled writer: test fixtures and ad-hoc scripts create configs with enabled=False directly, so the model keeps the pair consistent rather than every caller learning about status.
  • An is_new serializer field was written and then removed on Andy's call: nothing renders it yet, created_at is 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.
  • The field is named status, not scout_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 / ScoutConfigPauseReasonEnum were registered in ENUM_NAME_OVERRIDES up front, since a bare status ChoiceField is the classic enum-name collision.

@andrewm4894 andrewm4894 self-assigned this Jul 30, 2026
@trunk-io

trunk-io Bot commented Jul 30, 2026

Copy link
Copy Markdown

😎 Merged manually by @andrewm4894 - details.

@andrewm4894
andrewm4894 marked this pull request as ready for review July 30, 2026 10:38
@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team July 30, 2026 10:38
@pr-assigner-resolver-posthog

Copy link
Copy Markdown

👀 Auto-assigned reviewers

These soft owners were skipped because they only have minor changes here. Nothing blocks merge, so self-assign if you'd like a look:

  • @PostHog/team-platform-features (posthog/models/owners.yaml)

Soft owners come from each directory's owners.yaml and each product's product.yaml (resolved nearest-file-wins). The locator after each owner is the file that decided it. Generated files and lockfiles are ignored when deciding ownership.

@github-actions

github-actions Bot commented Jul 30, 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 2949a69 · box box-421a53d55d4d · ready in 1039s (push → usable) · build log · rebuilds on every push, torn down on close

@andrewm4894
andrewm4894 removed the request for review from a team July 30, 2026 10:40
Comment thread products/signals/backend/migrations/0076_scout_config_status_constraint.py Outdated
@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor
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

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🤖 CI report

Bundle size — no change

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

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 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.11 MiB · 3,020 files no change ████████░░ 83.5% 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.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 →

⚠️ 1 flaky test:

  • 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

Review snapshot changes →

⚠️ 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

Review snapshot changes →

⚠️ 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)

chatgpt-codex-connector[bot]

This comment was marked as outdated.

chatgpt-codex-connector[bot]

This comment was marked as outdated.

@andrewm4894 andrewm4894 added the reviewhog ($$$) Reviews pull requests before humans do label Jul 30, 2026
@posthog

posthog Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🦔 ReviewHog reviewed this pull request

Found 1 must fix, 1 should fix, 0 consider.

Published 2 findings (view the review).

chatgpt-codex-connector[bot]

This comment was marked as outdated.

@tests-posthog
tests-posthog Bot requested a review from a team as a code owner July 30, 2026 11:25
chatgpt-codex-connector[bot]

This comment was marked as outdated.

@posthog

posthog Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

ReviewHog Alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏

@posthog posthog 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.

ReviewHog Report

Business logic

Issues: 1 issue

Files (4)
  • products/signals/backend/migrations/0075_signalscoutconfig_status.py
  • products/signals/backend/models.py
  • products/signals/backend/admin.py
  • posthog/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.py
  • posthog/settings/web.py
  • products/signals/frontend/generated/api.schemas.ts
  • products/signals/frontend/generated/api.zod.ts
  • services/mcp/src/api/generated.ts
  • services/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 status field
  • Regenerates frontend and MCP API types/zod schemas to reflect the new enums and updated field descriptions

Comment thread products/signals/backend/models.py
Comment thread products/signals/backend/scout_harness/serializers.py Outdated
chatgpt-codex-connector[bot]

This comment was marked as outdated.

@github-actions
github-actions Bot requested a deployment to preview-pr-75349 July 30, 2026 13:56 In progress
chatgpt-codex-connector[bot]

This comment was marked as outdated.

@andrewm4894
andrewm4894 force-pushed the feat/scout-status-enum branch from aec3a5a to c7dd794 Compare July 30, 2026 15:24
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
chatgpt-codex-connector[bot]

This comment was marked as outdated.

@andrewm4894

Copy link
Copy Markdown
Member Author

/trunk merge

# Conflicts:
#	products/signals/backend/migrations/max_migration.txt

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +368 to +369
not_running_reason = "auto_paused"
pause_reason = config.pause_reason

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@andrewm4894
andrewm4894 merged commit d84d21e into master Jul 30, 2026
276 checks passed
@andrewm4894
andrewm4894 deleted the feat/scout-status-enum branch July 30, 2026 23:34
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 30, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-30 23:59 UTC Run
prod-us ✅ Deployed 2026-07-31 00:14 UTC Run
prod-eu ✅ Deployed 2026-07-31 00:16 UTC Run

andrewm4894 added a commit that referenced this pull request Jul 31, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

reviewhog ($$$) Reviews pull requests before humans do

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants