- Audit logs say what changed (
audit_log, EXPLICIT rows). - Open items say what is not finished (
db/openItems.ts, crew offers, checklist tasks, ledger items).
typesafe-ai/jev) only ranks what they found. CheckAI explains it through the
get_attention_items MCP tool. Resolving anything goes through the existing
confirm-gated MCP tools, never through this feature.
Linear: PRO-431. Code: api-events/src/systems/attention/,
api-events/src/db/attentionFacts.ts, api-events/src/db/attentionFeedback.ts.
Owner decisions (V1)
- Compute on read. Nothing about an item is stored and nothing runs on a schedule. Only viewer feedback persists.
- Surfaces: the web Home card (
AttentionBrief) and a read-only member MCP tool. No mobile. - Deterministic correlators find; Jev ranks. A correlator emits a candidate
with its evidence and a rule default class. Jev may assign a different class
and a confidence. If Jev is off, fails, times out, or answers below the
confidence floor, the rule class stands (
classified_by: "rule"). Jev never discovers items, never writes text, and never mutates. - Stale call sheets are deferred. Call sheets are not persisted, so there is no signal to correlate.
Pipeline
origin = 'EXPLICIT' literal and a bounded created_at window so the
partitioned table prunes (see db/orgActivityLog.ts). Every query binds the
caller’s organizationId; the unit tests assert it for each fact family.
A gig’s start is its startTime when set, otherwise the start of its civil day
in its timezone (event → organization → the default zone). All time arithmetic
is Temporal.Instant.
Correlators
Staffing is judged per offer day, not from the offer rollup. For each
upcoming
crew_offer_days row the loader counts ACCEPTED and PENDING
recipient-days against the day’s target: an open shift’s capacity (falling
back to max_acceptances), or a job offer’s seats_needed (per-day override,
then offer-level). A day is short when accepted is below the target, or, with no
target, while replies are pending. So a multi-day shift whose rollup says FILLED
still surfaces the one day that is short, and the evidence names each short day.
The offer evidence is stamped with the latest of the offer’s, its days’, and its
recipients’ and recipient-days’ updated_at / responded_at, so any staffing
change produces a new fingerprint.
A correlator suppresses itself once the situation resolves: the recipient
answered after the change or was re-notified, the seat filled, the tasks were
completed, the receivable was paid. Those are the “noisy” cases in
attentionCorrelators.test.ts.
Guest-count and billing-terms changes already write field-level changes on
event_updated rows (buildEventUpdateAuditEntries and
putEventBillingTermsOperation), so V1 needed no new audit emission.
Identity and dismissal
keyis a hash of the correlator, the gig, and the primary entity (the offer for crew time changes, the gig otherwise). It is stable while the situation exists.fingerprintis a hash of the sorted evidence ids and their timestamps. New evidence changes it.
key has the same fingerprint and an outcome other than
USEFUL. A new fingerprint brings it back.
Jev
attentionEvaluator.ts asks two questions per candidate:
classification: Choice overBLOCKING | NEEDS_ATTENTION | WATCH | FYI | IGNORE.importance: Score 0–3.
confidence is the chosen class’s probability when Jev returns one, otherwise
importance / 3. Below ATTENTION_JEV_MIN_CONFIDENCE the rule class stands.
Items Jev classifies IGNORE are dropped server-side.
The state Jev sees is a compact JSON of the candidate with no PII: the
correlator key, the rule class, hours to the gig, the viewer role
(owner | admin), counts, changed field names with a delta bucket (money moves
as a percentage bucket, never an amount), and days unresolved. No names, emails,
ids or amounts. The test sends only non-PII counts, buckets and timing pins
that. Calls use maxRetries: 0, AbortSignal.timeout, and gateway zero data
retention. The classify step also races the worker pool against a one-second
budget (ATTENTION_JEV_BUDGET_MS, not an env var). Once that budget ends, workers
stop taking candidates. Calls already in flight finish and can fill the cache, but
their answers are not applied to that response, so those candidates keep their rule
class. A candidate that throws keeps its rule class too, and the brief still returns.
A finished Jev answer is cached in process (at most 256 entries, keyed by
the non-PII state) and can apply on a later brief; in-flight calls are not aborted. Each brief
logs attention.jev.classified and records the attention.jev.classify timing
for the portion the request waited.
Configuration
attentionEngineConfig() in api-events/src/config/config.ts:
Availability
The attention engine is generally available to every org owner and admin. There is no feature flag on the web card or the API routes; the owner/admin check below is the only gate.HTTP
Both are org owner/admin only (
authorizeOrgAdmin), checked first, so a
non-member or a foreign org id gets the guard’s response and never loads org
data. The feedback route re-derives the item’s evidence from the current brief and
stores it with the classification the admin reviewed (classification,
classified_by, confidence from the loaded item). It does not re-run Jev, so
the stored class is the one the admin saw.
truncated is true when the org exceeds the 500-event or 1,000-audit-record
scan cap, so the brief covers only part of the org. It returns 404 when the key
is not in the viewer’s current brief and 409 STALE_ATTENTION_ITEM when the
fingerprint no longer matches. Outcomes: USEFUL, RESOLVED, DISMISSED,
NOT_IMPORTANT, ALREADY_HANDLED.
Items sort by class severity, then gig proximity, then confidence.
Feedback table
attention_feedback (AttentionFeedback): organization_id, user_id,
item_key, fingerprint, correlator, event_id, classification,
classified_by, confidence, outcome, snapshot (the item as shown),
created_at, indexed on (organization_id, user_id, item_key). The rows double
as evaluation data: Jev’s class, the evidence, and the admin’s verdict. Pushing
them into the external evals store is PRO-392, out of scope here.
Surfaces
- Web:
web/src/app/(app)/home/components/AttentionBrief.tsx, above Rounds for org owners and admins. Each row shows the reason, the gig, affected people or records, a “Why am I seeing this?” evidence list linking to the gig tab that holds each record, a navigation-only next step, and the feedback menu (optimistic SWR update). - MCP:
get_attention_items(read-only,organization_id?,limit?) returns the items with evidence ids so CheckAI can cite provenance.