Agent Parser Threat Stream Rescan Consistency
Use this skill when a re-scan result appears to update only part of Threat Stream behavior.
Typical symptoms:
- the sidebar card changes to
SAFEbut the row staysBLOCK LLM Analysisdoes not update after re-scanPRE-SCAN ANALYSISstill shows after a successful non-prescan re-scan- a row briefly jumps to a different list/work snapshot and then returns
- blocked-only counts, prescan counts, or audit summaries disagree with the detail panel
Core Rule
There must be exactly one current-state interpretation for a row.
metadata.context_analysisis the current truth when presentmetadata.prescan_context_analysisis fallback only for old rows that have never been replaced by a newer current context- legacy top-level
prescan_*flags are compatibility fields, not the source of truth for post-rescan behavior
Do not solve this with row-specific exceptions, UI-only overrides, or extra cleanup branches.
Required Invariants
- A re-scan is a row mutation, not a separate display-only overlay.
- The re-scan API should return the canonical updated row when possible.
- The frontend should replace the row from canonical server data, not partially merge risk/action fields into stale metadata.
- A non-prescan re-scan must stop emitting effective prescan state everywhere:
- list filters
- detail sidebar
- live stream/SSE payloads
- work snapshot rows
- read-model/preagg summaries
- audit metrics
- DB-derived fields like
prescan_blockedandprescan_module
- If a mutation occurs while viewing a work snapshot, rebuild the snapshot; do not keep reusing an immutable snapshot id as if it were live state.
High-Risk Failure Pattern
The most common root cause is split truth across layers:
- backend mutates
context_analysis - frontend still reads
prescan_context_analysis - live/SSE compaction still copies legacy
prescan_* - preagg/audit logic still classifies by raw legacy flags
- DB sync recomputes
prescan_blockedfrom stale metadata
If those are not unified, the same row can appear SAFE in one panel and BLOCK/PRESCAN in another.
Another common failure mode is a split filter contract:
- DB/read-model paging filters by stored
action - workset or frontend filters by recomputed effective action
- blocked rows survive one layer and disappear in the next
- sparse filtered pages then trigger endless auto-fill or repeated pagination attempts
Treat blocked as a product contract. Decide whether it means stored decision-time action or current effective action, then make every layer use the same rule.
Canonical Fix Pattern
Start by adding or using a single helper layer for current-state interpretation.
Recommended responsibilities:
resolve_effective_context_analysis(event)is_effective_prescan_blocked(event)resolve_effective_prescan_module(event)
Then route every consumer through that helper layer:
- backend filter logic
- Threat Stream read/sanitize logic
- workset/snapshot classification
- event/SSE compactors
- stats/preagg compaction
- audit metrics
- DB persistence-derived fields
- frontend row/detail rendering helpers
Frontend Rules
- Re-scan should not force a live view into work context unless the user explicitly requested a snapshot-style action.
- If the current mode is work context and a mutation happens, rebuild the workset after the mutation.
- The detail panel should derive both
PRE-SCAN ANALYSISandLLM Analysisfrom the same canonical current context rule as the row. - Badge/status/risk/detail rendering must use the same helper path.
- Auto-fill or infinite-scroll logic must only react to rows from the active query key. When filters/time range/search change, clear stale rows or gate pagination until the first page for the new query arrives.
- Do not send large
prompt_textvalues toconversation-contextwhen a stablemessage_idexists. Useprompt_textonly as a bounded fallback.
Good targets to inspect:
dashboard/src/lib/threatStream.tsdashboard/src/pages/ThreatStreamV2Page.tsx
Backend Rules
- Re-scan endpoints should persist the final current context and return the canonical updated row.
- Event compactors must not emit stale
prescan_context_analysisfor rows whose current context is no longer prescan. - Preagg and audit summaries must classify current rows, not historical prescan residues.
- DB import/update paths must derive
prescan_blockedandprescan_modulefrom the canonical current state, not by blindly OR-ing legacy metadata.
Good targets to inspect:
app/core/effective_state.pyapp/routers/analyze.pyapp/routers/events.pyapp/core/threat_stream_access.pyapp/core/threat_stream_workset.pyapp/core/recent_log_filters.pyapp/core/stats_preagg.pyapp/core/audit_metrics.pyapp/core/database.pyapp/repositories/log_repository.pyapp/services/conversation_context.py
Validation Checklist
After the patch:
- rebuild/redeploy
appif dashboard assets changed - confirm app and model-api are healthy
- test a real prescan sample and force a synthetic non-prescan current context
- prove all of these flip together:
- blocked filter match becomes false
- prescan filter match becomes false
- effective block source becomes
none - sanitized payload stops carrying
prescan_context_analysis - preagg/latest compact payload stops carrying
prescan_context_analysis
- verify the row no longer jumps into a stale work snapshot during single-row re-scan
- for live Threat Stream filters, verify both:
- switching to
blockeddoes not create repeated canceledqueryrequests - selecting a blocked row does not cause repeated
conversation-contextfetches or oversized prompt query strings
- switching to
Output Contract
Return:
- the single root-cause category
- which layers had split truth
- the canonical state rule adopted
- what was changed in frontend vs backend
- what was proven in runtime validation