Symptom
meetings.updated_at is bumped on every UPDATE to the row, including writes that record webhook delivery state (data->'webhook_deliveries' append, retry counters, etc). A row with active webhook retries can have its `updated_at` advanced indefinitely, defeating any consumer that uses `updated_at < threshold` as a "stale" / "no-progress" predicate.
Concrete past failure
#313 — the Pack E.3.2 stale-stopping sweep used `Meeting.updated_at < threshold`. Meetings genuinely stuck in 'stopping' kept looking fresh because webhook retry workers updated `data->'webhook_deliveries'`, bumping `updated_at`, and the sweep never fired. v0.10.6.1 worked around this by switching the sweep to a derived `last_progress_at` from `data->'status_transition'`.
Why this is bigger than the sweep
Any other code that uses `meetings.updated_at` as a "domain progress" signal is silently wrong. Candidates:
- analytics queries filtering meetings updated since N
- third-party integrations that poll on `updated_at`
- any future "stale" sweeps for other status states (joining / awaiting_admission / etc.)
- change-data-capture downstream consumers
The semantics drift from "row last changed" (current behavior) to what most consumers actually want: "domain progress last changed".
Proposed fix options
A. Move webhook delivery state to a separate table. `webhook_deliveries` already exists in some shape — verify and migrate.
B. Mark webhook-retry-touched columns as not bumping `updated_at` at the model/SQL level (PG generated columns or trigger-based).
C. Add an explicit `last_domain_progress_at` column that is only bumped on status transitions / data shape changes, leave `updated_at` as the row-level last-write timestamp.
Option C is least invasive and most explicit; option A is cleanest long-term.
Urgency
Medium. v0.10.6.1 sweep predicate routes around the symptom for the stale-stopping case, but every other `updated_at`-based query in the codebase is a latent bug. Should land in v0.10.7 alongside any analytics work that touches meetings.
Acceptance
- One of the three options chosen + implemented.
- Pack E.3.2 sweep predicate reverts back to a single-column filter (no JSONB post-filter needed).
- Registry check that catches future regressions: webhook write must not bump the chosen "domain progress" column.
Source
Surfaced during root-cause review of #313 in v0.10.6.1 develop stage.
Symptom
meetings.updated_atis bumped on every UPDATE to the row, including writes that record webhook delivery state (data->'webhook_deliveries'append, retry counters, etc). A row with active webhook retries can have its `updated_at` advanced indefinitely, defeating any consumer that uses `updated_at < threshold` as a "stale" / "no-progress" predicate.Concrete past failure
#313 — the Pack E.3.2 stale-stopping sweep used `Meeting.updated_at < threshold`. Meetings genuinely stuck in 'stopping' kept looking fresh because webhook retry workers updated `data->'webhook_deliveries'`, bumping `updated_at`, and the sweep never fired. v0.10.6.1 worked around this by switching the sweep to a derived `last_progress_at` from `data->'status_transition'`.
Why this is bigger than the sweep
Any other code that uses `meetings.updated_at` as a "domain progress" signal is silently wrong. Candidates:
The semantics drift from "row last changed" (current behavior) to what most consumers actually want: "domain progress last changed".
Proposed fix options
A. Move webhook delivery state to a separate table. `webhook_deliveries` already exists in some shape — verify and migrate.
B. Mark webhook-retry-touched columns as not bumping `updated_at` at the model/SQL level (PG generated columns or trigger-based).
C. Add an explicit `last_domain_progress_at` column that is only bumped on status transitions / data shape changes, leave `updated_at` as the row-level last-write timestamp.
Option C is least invasive and most explicit; option A is cleanest long-term.
Urgency
Medium. v0.10.6.1 sweep predicate routes around the symptom for the stale-stopping case, but every other `updated_at`-based query in the codebase is a latent bug. Should land in v0.10.7 alongside any analytics work that touches meetings.
Acceptance
Source
Surfaced during root-cause review of #313 in v0.10.6.1 develop stage.