From e5d670c609929053676feb102f20bf62794eafd5 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 1 Jul 2026 00:10:14 +0200 Subject: [PATCH] docs(revamp): reshape Triggers into Phase 6 --- plans/backend/revamp/06-triggers.md | 105 +++++++++++++++++++--------- 1 file changed, 72 insertions(+), 33 deletions(-) diff --git a/plans/backend/revamp/06-triggers.md b/plans/backend/revamp/06-triggers.md index 087724143..00dd5008c 100644 --- a/plans/backend/revamp/06-triggers.md +++ b/plans/backend/revamp/06-triggers.md @@ -1,20 +1,32 @@ -# Domain ⑤ — Triggers (the pluggable refresh clock) (CI pivot revamp · WIP) +# Phase 6 — Triggers (the pluggable refresh clock) -> **WIP design doc.** Part of the Phase 4 → end revamp. The thinnest domain. -> **Scope guardrail:** Phases 1–3 SHIPPED/FIXED. This domain is **decoupled** — Intelligence (`03`) -> has no dependency on it, and **automations is at most one optional adapter, never required**. +> **Phase 6** of the CI-pivot revamp — the thinnest phase. **Build after** Phase 5 (`05b` exposes +> `refresh(tracker)`). +> **Depends on** `05b` (`refresh(tracker)`), `04b` (the REST manual/cron routes), and the SHIPPED +> automations subsystem (its schedule selector + `AutomationRun` + delivery). +> **Scope guardrail:** Phases 1–3 SHIPPED/FIXED. This phase is **decoupled** — Intelligence (`05b`) has no +> dependency on it, and **automations is at most one optional adapter, never required**. -## Purpose +## Objective Decide **when** a Tracker refreshes. Intelligence exposes a single entry point — **`refresh(tracker)`** — -and every trigger is just a caller. Intelligence never knows which trigger fired; remove any trigger -and the engine still works. +and every trigger is just a caller. Intelligence never knows which trigger fired; remove any trigger and +the engine still works. This replaces the old Phase-6 cron scheduler — and the resolution is **not** to +rebuild a scheduler at all, but to **reuse the automations subsystem** for the in-app recurring path. -This replaces Phase 6's cron scheduler — and the resolution is **not** to rebuild a scheduler at all, -but to **reuse the automations subsystem** for the in-app recurring path (it already has a hardened -clock + run record + delivery). A trigger only ever calls `refresh(tracker)`. +## Current state (cited) -## The adapters +- **`refresh(tracker)`** — the headless unit of work exposed by `05b`. +- **Automations schedule selector** — the already-hardened cron selector (`FOR UPDATE SKIP LOCKED` + claiming, `next_fire_at` advance, self-heal, duplicate-run suppression, `catchup=False`), reusing the + `croniter` util. +- **`AutomationRun`** — the existing run record + PENDING-gate (audit + idempotency). +- **Automations output/delivery** — the existing path that carries results to the user. +- **Access routes** (`04b`) — where the external-cron and manual REST endpoints live. + +## Target design + +### The adapters | Adapter | Fired by | MVP? | |---------|----------|------| @@ -23,38 +35,61 @@ clock + run record + delivery). A trigger only ever calls `refresh(tracker)`. | **External cron** | the user's own scheduler hits `POST /v1/trackers/{id}/refresh` | ✅ (zero infra on us) | | **CI automation action** | a **CI action on the existing automations** — its schedule trigger fires `refresh(tracker)` **and delivers** the material changes | ✅ (the in-app recurrence + alert path) | -## Recurrence + delivery — a CI action on existing automations (NOT a new scheduler, NOT a new shape) +### Recurrence + delivery — a CI action on existing automations (NOT a new scheduler, NOT a new shape) -The SMB competitor-watch buyer needs **in-app recurrence** *and* **"tell me when it changes"**. Instead -of building a bespoke tick (which — done correctly — still needs `FOR UPDATE SKIP LOCKED` claiming, -`next_fire_at` advance, self-heal, duplicate-run suppression, `catchup=False`), we **add a CI *action* -to the existing automations subsystem**: +The SMB competitor-watch buyer needs **in-app recurrence** *and* **"tell me when it changes."** Instead of +building a bespoke tick, we **add a CI *action* to the existing automations subsystem**: - **Schedule** → the automation's existing **schedule trigger** (the already-hardened selector). No new - scheduler. **(closes the old Gap B — scheduler rigor.)** -- **Run record + idempotency** → the automation's existing **`AutomationRun`** + PENDING-gate. No new - run table. **(closes the old Gap A — run/idempotency, see `03`.)** -- **Delivery / alert** → the automation's existing **output/delivery** carries the material changes to - the user. **(closes the old Gap E — alert delivery.)** + scheduler. **(closes old Gap B — scheduler rigor.)** +- **Run record + idempotency** → the automation's existing **`AutomationRun`** + PENDING-gate. No new run + table. **(closes old Gap A — run/idempotency, see `05b`.)** +- **Delivery / alert** → the automation's existing **output/delivery** carries the material changes to the + user. **(closes old Gap E — alert delivery.)** -**Why a CI *action*, not a new automation shape:** a new shape would duplicate triggers, runs, and -delivery that already exist. A single `refresh_tracker` action reuses all of it. (If implementation finds -the action too constraining, a thin CI-specific shape is the fallback — but the action is the default.) +**Why a CI *action*, not a new automation shape:** a new shape would duplicate triggers, runs, and delivery +that already exist. A single `refresh_tracker` action reuses all of it. (If implementation finds the action +too constraining, a thin CI-specific shape is the fallback — but the action is the default.) ### Decoupling is preserved (automations is still optional) -CI **core** — `refresh(tracker)` + Timeline (`03`/`04`) — has **zero** automations dependency and runs via -manual / agent / external-cron. Automations is the **optional adapter** that adds recurrence + delivery +CI **core** — `refresh(tracker)` + Timeline (`05a`/`05b`) — has **zero** automations dependency and runs +via manual / agent / external-cron. Automations is the **optional adapter** that adds recurrence + delivery + audit for in-app users. So we honor "don't glue CI to automations" *and* get its machinery for free. -## Where it lives / decoupling +### Where it lives - The **CI action** lives with automations (its action registry); it imports `refresh(tracker)` from `app/intelligence/`. No new scheduler/Beat task. - The **external-cron** and **REST manual** paths are just Access-door routes (`POST - /v1/trackers/{id}/refresh`) — Domain ② plumbing. + /v1/trackers/{id}/refresh`) — `04b` plumbing. -## Locked decisions +## Work items + +1. **Manual / agent triggers**: a `refresh_tracker(tracker_id)` chat tool + REST route → `refresh(tracker)`. +2. **External-cron route**: `POST /v1/trackers/{id}/refresh` (API-key authed) → `refresh(tracker)`. +3. **CI automation action**: register a `refresh_tracker` action in the automations action registry that + calls `refresh(tracker)` and routes the resulting material changes into automations delivery. +4. **Concurrency guard**: a per-Tracker in-flight lock (belt-and-suspenders over the automation run-gate). + +## Tests + +- **Decoupling**: `refresh(tracker)` works via manual/agent/external-cron with automations disabled entirely. +- **Recurring path**: a scheduled CI action fires `refresh` on cron and delivers material changes; an + unchanged refresh delivers nothing. +- **Idempotency**: a redelivered scheduled run (same `AutomationRun`) does not double-refresh. +- **External cron**: `POST /v1/trackers/{id}/refresh` triggers exactly one refresh; rejects bad auth. +- **Concurrency**: a second refresh while one is in flight is skipped/queued, not run concurrently. + +## Risks / trade-offs + +- **Action vs shape**: the CI action is the default; a thin CI automation shape is the fallback if the + action proves too constraining. +- **Delivered payload shape** (agent-summarized vs raw deltas since last fire) — implementation-time call. +- **Double-guarding concurrency**: the per-Tracker lock overlaps the automation run-gate, but the lock also + protects the manual/cron paths that don't go through automations. + +## Resolved decisions 1. Intelligence exposes `refresh(tracker)`; all triggers are callers. Fully decoupled. 2. Adapters: manual · agent · external-cron · **CI automation action** (recurrence + delivery). @@ -63,10 +98,14 @@ manual / agent / external-cron. Automations is the **optional adapter** that add 4. Recurrence is a **CI *action*** on the existing automations, **not a new automation shape**. 5. CI core stays runnable with **zero** automations dependency (manual/agent/external-cron). +## Out of scope (hand-offs) + +- **`refresh(tracker)` internals** (the hot loop, materiality) → `05b`. +- **The Timeline reads** the delivery summarizes → `05a`. +- **The chat tool surface** for manual/agent refresh → `07` (the `intelligence_agent` toolset). + ## Open questions (carry forward) - CI **action** vs a thin CI-specific automation **shape** (default: action; shape is the fallback). -- What the delivered payload looks like (the material `entity_changes` since last fire — summarized by - the agent, or raw deltas). -- Concurrency: skip a refresh if the Tracker already has one in flight (per-Tracker lock, like the connector - indexing lock) — even with the automation run-gate, belt-and-suspenders. +- What the delivered payload looks like (summarized material changes vs raw deltas since last fire). +- Concurrency: per-Tracker lock granularity (like the connector indexing lock).