Skip to main content
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 supplies WatchServices with actual host bindings. Calling the factory validates the configuration; watch.activate() is the explicit operation that subscribes and schedules work.
Replace the example identity and grant references with your host’s authorized values. Watch identity uses 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 contain key, identity, watchId, configVersion, kind, and at. Persist them with idempotent upserts. Resolve the exact identity/watch/version before dispatching:
The library does not start a scheduler worker. Desired schedule times persist if job publication fails; retrying 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.