Every incident in GeoSentinel moves through a defined set of states over its lifetime. A newly clustered incident starts asDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/danizd/GeoSentinel/llms.txt
Use this file to discover all available pages before exploring further.
open. When a new observation arrives, it briefly enters updated — a transient signal that the incident is actively receiving data — before settling back to open. If no new observations arrive for 72 hours, the incident becomes stale. Operators can close resolved incidents, reopen them, or flag them as false_positive to exclude them from the public API. All state changes that involve operator action are recorded in the append-only corrections_audit table. The lifecycle job (backend/jobs/incident_lifecycle.py) runs periodically to apply time-driven transitions automatically.
States
| State | Description |
|---|---|
open | The incident is active. At least one observation has been received within the last INCIDENT_STALE_HOURS (default: 72 hours). This is the default state for all newly created incidents. |
updated | A transient state entered whenever a new events_canonical record is linked to the incident. Signals real-time activity. Automatically reverts to open after 15 minutes via the lifecycle job. |
stale | No new observations have arrived for longer than INCIDENT_STALE_HOURS. The incident remains in the database and is visible via the API; it is not deleted. A new observation reactivates it to open. |
closed | The incident is confirmed as ended, either by an operator action or a configurable business rule. Unlike stale, closed implies intentional termination. Requires an audit record. Can be reopened by an operator. |
false_positive | Flagged by a human operator as a detection error — the underlying event did not actually occur as reported. Excluded from the public API by default (include only when include_fp=true is passed). Requires an audit record in corrections_audit. |
backend/models/incidents.py as string constants:
State Transitions
Transition Rules
* → updated
Trigger: A new event_canonical record is associated with the incident by the clustering job (via assign_event_to_incident()).
Actions performed atomically:
last_seenis updated tomax(current last_seen, event.event_time).observation_countis incremented.sourcesarray is updated to include the new source.severity_latestis set to the new event’s severity.severity_maxis updated if the new event’s severity exceeds the current maximum.fatalities_totalis updated tomax(current, event.fatalities).confidenceis recalculated over all linked events.canonical_pointis recomputed as the confidence-weighted centroid.
updated → open
Trigger: 15 minutes have elapsed since the last transition to updated (i.e., status_changed_at < now() - 15 minutes).
Implementation: The lifecycle job queries all incidents with status = "updated" and status_changed_at < threshold, then calls transition_to_open() with reason="auto_transition_after_15min".
open | updated → stale
Trigger: now() - last_seen > INCIDENT_STALE_HOURS (default: INCIDENT_STALE_HOURS = 72).
Condition: The incident’s status must be open or updated. An already-stale, closed, or false_positive incident is not affected.
Note: Incidents are never deleted when they go stale. They remain queryable via the API with status=stale.
stale → open (Reactivation)
Trigger: A new observation enters the cluster of a stale incident (i.e., a new events_canonical record is close enough in space-time to be assigned to the incident).
Condition: The new observation’s event_time must fall within the active clustering window for its category.
Effect: The incident transitions directly to updated (via transition_to_updated()) and then to open after 15 minutes. The transition is logged with reason='new_observation'.
* → false_positive
Trigger: Operator action via POST /v1/corrections with correction_type = "false_positive".
Mandatory: A row must be inserted into corrections_audit with the before_state, after_state, corrected_by (operator user ID), and an optional reason.
Effect: The incident is excluded from the default API response. It is only returned when the caller explicitly passes include_fp=true.
false_positive → open (Reversal)
Trigger: Operator action via POST /v1/corrections to reverse a false-positive designation.
Condition: incident.status must be exactly "false_positive".
Implementation: Calls resolve_false_positive(), which sets status = "open" and creates an audit record with correction_type = "reclassify".
* → closed
Trigger: Operator action via POST /v1/corrections with correction_type = "close", or a configurable business rule (e.g., event source officially closes the record).
Difference from false_positive: closed means the incident genuinely occurred but has ended. false_positive means the incident should not have been created at all.
Audit: A corrections_audit record with correction_type = "close" is always created.
closed → open (Reopen)
Trigger: Operator action via POST /v1/corrections to reopen a mistakenly closed incident.
Condition: incident.status must be exactly "closed". The lifecycle job does not automatically reopen closed incidents.
Implementation: Calls reopen_closed(), which validates the current status, sets status = "open", and creates an audit record.
Invalid Transitions
The following transitions raise aValueError when attempted. They cannot be executed by the lifecycle job or the clustering job — only an explicit operator correction can override them.
| From | To | Result |
|---|---|---|
closed | open (automatic) | ValueError — must use reopen_closed() with corrected_by |
closed | updated | ValueError — always invalid |
closed | stale | ValueError — always invalid |
false_positive | updated | ValueError — always invalid |
false_positive | stale | ValueError — always invalid |
stale | updated | ValueError — transition_to_stale guards against this |
false_positive | open (automatic) | ValueError — must use resolve_false_positive() with corrected_by |
Running the Lifecycle Job
The lifecycle job is implemented inbackend/jobs/incident_lifecycle.py:
DATABASE_URL from the environment and performs two passes:
- Stale pass: Finds all
openorupdatedincidents wherelast_seen < now() - 72 hours. Callstransition_to_stale()for each. - Updated-to-open pass: Finds all
updatedincidents wherestatus_changed_at < now() - 15 minutes. Callstransition_to_open()withreason="auto_transition_after_15min"for each.
{"stale_transitions": N, "updated_to_open": M}.
