DurableWatch manages a versioned source-to-notification lifecycle. It uses deterministic filters, persisted cursors, bounded pending events, and an outbox. It makes no model calls. It is separate from HarnessRuntime; it requires its own store, scheduler, source, connector, and authorization.
Define and bind a watch
The following integration factory is complete TypeScript. Your application suppliesWatchServices with actual host bindings. Calling the factory validates the configuration; watch.activate() is the explicit operation that subscribes and schedules work.
actorId, whereas harness-run identity uses userId.
defineWatch() validates, copies, and freezes JSON configuration. describeWatch() returns { definition, hash, activation: "explicit", modelCalls: false }. Neither activates a source. Unknown fields, invalid identifiers, invalid timezones, or out-of-bounds limits fail early.
Supply host services
Grant reference strings are not authorization by themselves. Source and notification scope are checked against the definition on operations. Changing connector version or filter behavior requires a new configuration version.
Dispatch scheduled jobs
Schedules containkey, identity, watchId, configVersion, kind, and at. Persist them with idempotent upserts. Resolve the exact identity/watch/version before dispatching:
activate() repairs publication. Activation persists its baseline cursor before creating the source subscription, so retry does not silently skip ahead.
Read changes and deliver a digest
poll() reads from the persisted cursor. trigger(raw) first verifies a source hint, validates principal and scope, then reads from that same persisted cursor. A push-provided cursor cannot advance state. Acknowledge a push only after trigger() resolves.
Stable semantic event IDs deduplicate admission. Cursor advancement, decisions, and pending digest events commit together under a fenced lease. Unchanged wakes send nothing. Resync events are recorded as suppressed decisions rather than historical notifications.
flush() prepares at most one immutable digest, reserves its local-day notification allowance, and uses the core durable action ledger for dispatch. The action binds identity, destination, connector version, grants, policy revision, and arguments. Authorization runs again immediately before sending.
A lost acknowledgement produces an unknown outcome. Newer digests wait until the host connector supplies durable evidence. reconcile(outboxId) checks that evidence and never sends a notification. Leases fence local commits; they cannot retract requests already sent to a provider.
Lifecycle methods
An update is a fail-closed rollout, not an atomic transaction across versions. Same-source updates inherit cursor, deduplication IDs, cooldown, and daily reservations. Old pending/unknown sends keep their original aggregate, grants, and destination. Deleting a retired version does not stop its replacement’s subscription.
Timing and limits
All limits are positive safe integers. State is bounded per version; arrange explicit version rotation or archiving before exhausting limits. Maintenance operations can still pause, delete, or reconcile after the work budget is exhausted, without admitting new sends.
Daily reservations use the configured IANA timezone and durable store time. Quiet windows account for daylight-saving transitions. Denied or uncertain sends conservatively retain their reservation; delivery delayed to another day reserves that day too. Changing timezone requires a new watch identity.
For a source implementation, implement
baseline, activate, stop, bounded read, and compareCursors; add verifyTrigger for push. See Gmail source, full watch contracts, and core durable protocols.