# ADR-0081: Eve watcher semantics before UI

- Status: accepted for Task 7.3 evidence
- Date: 2026-09-09
- Owners: Eve assistant runtime, Security, Operations, Privacy, Evaluation
- Scope: Phase 7 Task 7.3 only

## Context

`@oshun/assistant` already contains a scheduler for one-shot and cron prompts,
but a schedule is not a watcher. A watcher has a governed evidence source, a
truth condition, evidence-age rules, repeated delivery semantics, revocable
authority, privacy retention, and restart behavior. Exposing create/list/pause
controls before these rules exist would make the UI the accidental source of
truth and would invite incompatible event and polling behavior.

Task 7.3 therefore establishes a transport-free reference contract and state
machine. It does not expose a Drawer tool, start a worker, call an external
source, or send a notification.

## Decision

### Source and condition

A versioned `WatcherDefinition` is owned by an exact tenant and user and binds
one purpose, source, condition, delivery reference, expiry, budgets, retries,
and privacy policy.

Sources are a closed union:

- An event source names an opaque source id, exact event type, and maximum
  evidence age. Event evidence carries an event id and durable source cursor.
- A polling source additionally names a governed tool, five-field cron
  expression, IANA timezone, and explicit `skip`, `latest`, or bounded
  missed-run policy. A bounded policy retains at most the newest 100 due
  instants. This task selects already-derived due instants; the future worker
  owns cron enumeration and source invocation.

Conditions use a depth- and fanout-bounded declarative DSL over safe dotted
payload paths. The only operators are strict scalar comparisons, string
containment, existence, Boolean all/any, and not. No code, prompt, regular
expression, or model judgment participates in condition truth.

### Evidence and time

Evidence must match the definition's source kind and source id; event evidence
must also match the event type. It is fresh only inside the configured past-age
window and a fixed 30-second future-skew allowance. An authorized stale or false
record advances its source checkpoint so a poison record cannot replay forever.
An authorization failure returns no transition and therefore cannot advance a
checkpoint.

Quiet hours use local minutes in an explicitly validated IANA timezone. The next
allowed delivery is found by walking real UTC instants and recomputing local
wall time for each instant. This covers nonexistent spring-forward minutes and
both occurrences of fall-back minutes instead of assuming a 24-hour local day.

### Delivery, dedupe, retry, and restart

A true condition creates a durable outbox entry. Its SHA-256 delivery key binds
watcher id/version, source id, and the source's stable trigger identity: event
type/id or poll scheduled instant. Mutable ingress-envelope ids, cursors,
observation timestamps, and payload fields do not change that key, so a retried
source event cannot evade dedupe. A separate digest binds the complete canonical
evidence for audit. Delivery is at least once, not exactly once: a crash after
the destination accepts a message but before the durable acknowledgement can
cause redelivery. Every delivery adapter must pass the stable delivery key as
its downstream idempotency key.

Dispatch follows this persistence sequence:

1. Recheck current delivery authority.
2. Claim one due outbox entry as `dispatching` and increment its attempt count.
3. Persist that transition with the store's subject-scoped revision
   compare-and-set.
4. Only after the compare-and-set wins, invoke the destination with the stable
   delivery key.
5. Persist `delivered`, retryable `pending`, or terminal `dead_letter` state
   through another compare-and-set.

A process restart changes interrupted `dispatching` entries back to `pending`
without changing their keys or attempt counts. Failed attempts use deterministic
capped exponential backoff. The configured retry ceiling dead-letters an item;
the independent per-window attempt ceiling stops dispatch until the next budget
window. Only bounded categorical transport error codes are retained.

### Budgets and authority

Each fixed window caps evaluations, unique deliveries, delivery attempts,
tokens, and micro-USD cost. Usage is non-negative integer accounting supplied by
the governed caller. Exhaustion suppresses new work and is recorded as a reason;
a new window resets counters.

Authorization is fail-closed and is rechecked before every evidence evaluation,
every delivery attempt, and every management or erasure command. The grant must
match the exact tenant, user, purpose, source, governed polling tool when
applicable, delivery target when delivering, requested action, and current time.
The definition stores opaque registry references, never credentials or raw
webhook URLs.

### Lifecycle, persistence, and privacy

Lifecycle is `active`, `paused`, `cancelled`, or `expired`. Pause/resume is an
authorized reversible transition. Cancellation is terminal. Expiry applies to
active and paused watchers and prevents further evaluation or delivery.

The persistence key is `(tenant_id, user_id, watcher_id)`, so the same opaque
watcher id in another subject is distinct. Initial creation refuses a conflict
instead of overwriting an existing revision. Updates use an advancing revision
and compare-and-set to reject stale workers. Reads, listings, and erasure use
the exact tenant/user predicates. The process-local reference store can export
and restore a snapshot; the Postgres port retains definition, state, outbox,
checkpoint, budgets, and revision as durable JSONB.

Evidence receipts retain only scalar leaves at policy-allowlisted payload paths;
allowlisting a parent object does not retain its unlisted descendants. Stale or
budget-rejected evidence retains no payload. Delivery metadata retention must
cover the full evidence-age window plus future skew, so a still-acceptable
replay cannot outlive its dedupe key. Retention windows purge evidence and
settled delivery metadata; pending and dispatching work is not silently deleted.
Cancellation can immediately remove evidence and the entire undelivered outbox.
An authorized subject erasure removes receipts, outbox, suppressions, and
checkpoints, and storage can delete all rows for that exact subject.

## Consequences and honest boundary

The repository now has one testable semantic authority for watcher definitions,
evaluation, delivery state, recovery, tenancy, and privacy. Event and polling
workers can share the same state transitions instead of inventing behavior in
their adapters. The design deliberately exposes no unbounded code execution or
destination credential.

This is a source/static and process-local/SQL-boundary contract, not live
watcher reliability evidence. It does not enumerate cron fires, poll a real
source, consume a real event stream, send a notification, prove a real database
restart, or exercise a distributed worker race. It does not cite evidence in a
user-facing notification. Task 7.4 owns gated product tools and notification
shape; Task 7.5 owns multiple live watcher classes, duplicate/flapping/outage
behavior, restart recovery, poisoned inputs, revoked permissions, and
cancellation under realistic service conditions. Task 7.3 therefore closes only
the pre-UI semantics contract; Phase 7 and G3/G11-G18 remain open.
