Skip to content

[Epic] Control plane absorbs Watcher's operator surface #359

Open

Description

@gregoryfoster

Child of CannObserv/watcher#368: Headless Watcher and cohort boundaries.

Design of record (in watcher): 2026-10-14-headless-watcher-and-cohort-boundaries-design.md

Issues

Phase 1 — build additive and dark, in the order under Order below:

  • Domains: become the sole owner (min_interval, default cadence, deactivation) #360 Domains: become the sole owner (min_interval, default cadence, deactivation) · M · PR #360 Domain ownership: suspension resolves active=false; fetch-policy columns; Watcher import #434 (492ac55); D1–D4 taken as recommended. CI green by job on a935d68 and 492ac55. Deployed 2026-10-25T18:52Z; live now 284bdc06dfc3 (with fix: API key reveal box shows the new key (empty since #37) #435). OpenAPI 4.33.0, SDK 5.12.0, alembic 173c1af91def (min_interval_seconds float NOT NULL default 1.0 with a finite/non-NaN CHECK; default_watch_spec JSONB). Operator import 2026-10-26: 4|t|t|t [measured]. 0 info.registry outbox rows since the deploy; snapshot 5 live; Watcher applied_active true ×5; sweep 16 checked, 0 invalid. Coordinator errors in the brief: (1) is_active gates nothing (the description was stale; the index only displays it); (2) the dashboard has no is_active control; (3) missed the watch panel reading suspension as a permanent "resume pending"; (4) missed the domain-toggle-vs-announce race and its lock order, which most of CR 1–31 hardened; (5) missed that info_sources.domain_name is nullable with no FK, so PATCH's insert branch can flip suspension; (6) docs/SCHEMA.md had no Domain entry to update; (7) the import writes 1 row, not 3 + 1; (8) missed the two export paths. Standing: Watcher re-exports and the operator re-imports before F1 and F2 (host step 9).
  • Produce content.fetch-policy (dark until F1) #366 Produce content.fetch-policy, dark until F1 · M · PR #366 Produce content.fetch-policy, dark until F1 #436 (b0f5782). CI green by job on 3d45d7a and b0f5782. Live b0f57823c0f1 = /health = main, deployed 2026-10-26T16:57Z; OpenAPI 4.34.0; alembic 44b797877fdb. domains.fetch_policy_generation is 4 distinct non-null values (1–4); sequence CACHE 1; 0 tombstones; 0 content.fetch-policy outbox rows; journal "producer is dark"; the unit line is commented out; sweep 16 checked, 0 invalid [measured]. D1–D3 taken; D4 dropped by the implementer, rightly: Watcher's deleted hosts already end on a revoked frame, and the F1 runbook replaces it with a dry-run read-back. Coordinator errors in the brief: (1) the delta maxlen rule would have trimmed below the floor (deltas carry the last full set's cap); (2) D4's premise; (3) missed that DEFAULT nextval() is drawn before an insert waits on a racing delete (every draw is now UPDATE … RETURNING once the row is held); (4) "loudly once per tick" with no bus: there is no tick; (5) "first full set before any delta" is unneeded with generations. For the F1 flag PR: the schema and docstring "Dark until" text (contract path, so CHANGELOG plus an SDK regen) and the unit-test flip.
  • F1: switch on the content.fetch-policy producer (after watcher#374 is live) #438 F1: switch on the content.fetch-policy producer · S · PR #438 F1: archiver.service produces content.fetch-policy (merge after watcher#374 + re-import) #439 (eb2215a), merged 21:36:16Z, after F1: stop the content.fetch-policy producer watcher#374 part 1 went live (18:57:22Z) and the re-export (21:33:18Z): D8's order held. CI green by job on 689845e and eb2215a. Live eb2215a8f86c = /health = main since 21:42:24Z; OpenAPI 4.35.0; ARCHIVER_FETCH_POLICY_PRODUCER=1 in the unit; producer started 21:42:27Z; full sets of 4 every 300 s, maxlen 500; no warnings; drift ok; bus-health 0 findings [measured]. Gap ~2 h 47 m on Replicator's default. Coordinator errors in the body: (1) "version bump" is the service only (4.35.0; SDK stays 5.12.0); (2) docs/DOMAINS.md didn't fully follow the flag; (3) the dashboard copy follows the process's flag, so dev/test render false text after F1; (4) missed further "dark until F1" sites in src/. Read-backs owed: see Verification debt.
  • Watch lifecycle: archive and restore #361 Watch lifecycle: archive and restore · S · PR #361 feat: watch lifecycle — archive and restore an InfoItem #442 (764a0f9), merged 2026-10-27T21:12:50Z; D1–D5 taken. Q1 answered by the operator: D5's refusal lives in import_watcher_item_cadence, whose Resolve cadence and announce a concrete interval #364 line "an archived item imports like any other" is reversed. CI green by job on cdd9de1 and 764a0f9. Live 764a0f935fb6 = /health = main since 21:24:48Z (deployed by the implementer with the operator's authorization [stated]); OpenAPI 4.37.0, SDK 5.14.0 (archive_info_item/restore_info_item); alembic 44b797877fdb → da4d2a2e19c7; archived_at timestamptz NULL; no warnings since deploy; drift ok; bus-health 0 findings [measured]. Inert as predicted: info.registry still count 3, max 01M4ZW9JY8W2EXP2QVYN79WEC3; archived_at NULL 5/5; applied_active true ×5 [measured]. 8001 rehearsal (implementer, issuecomment-6452745452) [stated]: paused 2:false 3:false 4:false, active 2:true 3:false 4:true, repeats silent. Notes left for Temporal profiles: model, API/UI, daily boundary job, post-actions #365 (service entry points, safe with the item locked, idempotent) and Check-now via info.watch-check #370 (pre-flight archived_at). Watcher told to archive only in Archiver until the jump, and to hold 0 archived before the re-export ([Epic] Headless Watcher and cohort boundaries watcher#368 issuecomment-6454837910). Coordinator errors in the brief: (1) "the importer" for D5 without naming it, and missed that import_watcher_item_cadence's docstring said the opposite (Q1); (2) named Domain archive as the template without noting it overwrites archived_at on every call, where D2 keeps the first; (3) missed that D3 and restore on a suspended host re-announce with active unchanged (CR 3, stet); (4) missed the domain page's item table as a badge site (CR 2); (5) didn't flag that SCHEMA, PAGES, ARCHITECTURE and BUS had 2–39 tokens of budget headroom (CR 1); (6) didn't flag bare "Archive" beside archive.org replication (CR 6, now "Archive watch"). Still pending: step 5, the item importer's dry run at the pre-F2 re-export (0 archived).
  • Resolve cadence and announce a concrete interval #364 Resolve cadence and announce a concrete interval · S–M · PR #364 feat: announce the resolved cadence (profile → item → domain) #441 (d49e292), merged 2026-10-27T00:40:56Z; D1–D5 taken, plus the implementer's D5' (default_watch_spec fill-if-absent in the domain importer). CI green by job on 5b9a4b4 and d49e292. Live d49e292ac4b6 = /health = main since 02:39:55Z; OpenAPI 4.36.0, SDK 5.13.0 (set_watch_spec); alembic 44b797877fdb (no migration); no warnings since deploy; 13 registry snapshots, no resolver WARNING; drift ok; bus-health 0 findings [measured]. Inert as predicted: 0 info.registry outbox rows past the baseline 01M4ZW9JY8W2EXP2QVYN79WEC3; all 4 domain defaults NULL; applied_interval 1d ×3, 1h, 6h, unchanged, each matching its watch_spec [measured]. Host steps (operator, issuecomment-6443906407) [stated]: domain importer dry run 4 unchanged, no differs; item import found nothing to fill (Watcher's own-cadence item is the 1h one, which Archiver already holds); 8001 fan-out checked end to end. Coordinator errors in the brief: (1) the builder took a bool (host_suspended), not the Domain row; (2) nothing validated the announced watch_spec (validation is write-time only); (3) gave the lock order but not the mode: a cadence write needs FOR UPDATE, because an announce's FOR KEY SHARE doesn't wait on FOR NO KEY UPDATE; (4) missed that the SDK had no PUT …/watch-spec wrapper, which D3's importer needs; (5) missed the pre-F2 ordering hazard (an Archiver domain default outranks Watcher's own item cadence until the item import runs); (6) missed the "dark until F2" text (DOMAINS.md, the domain page, DomainOut/DomainPatch); (7) D5 missed that the domain importer also overwrites and clears default_watch_spec; (8) the editor example "Watcher's default 1d" would mirror Watcher's constant (Refactor brittle Watcher↔Archiver "mirror discipline" for shared content-acquisition code watcher#159). Notes for Temporal profiles: model, API/UI, daily boundary job, post-actions #365 relayed (issuecomment-6445429892). · Coordinator miss in verification (found at Temporal profiles: model, API/UI, daily boundary job, post-actions #365's pre-flight): docs/API.md:23 still says domain cadence is "stored but dark until … Resolve cadence and announce a concrete interval #364 (F2)"; routed to Temporal profiles: model, API/UI, daily boundary job, post-actions #365's living docs.
  • Temporal profiles: model, API/UI, daily boundary job, post-actions #365 Temporal profiles: model, API/UI, daily boundary job, post-actions · M · PR #365 feat: temporal profiles - model, API/UI, daily boundary job, post-actions #443 (f6bb673), merged 2026-10-28T16:01:03Z; D0–D5 taken, plus the implementer's latitude calls (issuecomment-6455054077): PK = info_item_id; PUT create-or-replace clears fired_at and 422s an already-ended window; tighter rule validation (one seasonal rule, no duplicate days_before, no zero or >10y interval); is_active=false contributes and fires nothing. CI green by job on 1c74575 and f6bb673. Live f6bb673c5cfb = /health = main since 16:19:17Z (deployed by the implementer with the operator's authorization [stated]); OpenAPI 4.38.0, SDK 5.15.0 (get_/set_/delete_temporal_profile); alembic da4d2a2e19c7 → b7e3c95a1f20; temporal_profiles 0 rows; timer enabled, next 2026-10-29 00:05 UTC, Persistent=true, unit User=archiver with the unit-scoped ARCHIVER_ALLOW_PRODUCTION_DB; hand run Result=success, evaluated 0, announced 0, skipped 0; no warnings since deploy; drift ok; bus-health 0 findings [measured]. Inert as predicted: info.registry still count 3, max 01M4ZW9JY8W2EXP2QVYN79WEC3; applied_interval 1d ×3, 1h, 6h and applied_active true ×5, unchanged [measured]. D5 dry run on the 10-14 export (issuecomment-6475356848) [stated]: no refusal; 0 profiles, 0 floors, 0 archived; 0 to fill, 1 unchanged. docs/API.md:23's stale line fixed. Curation split DEPLOYMENT, SCHEMA, PAGES and BUS into MONITORING, SCHEMA_SOURCES, SOURCE_PAGES and BUS_CLIENT. Coordinator errors in the brief: (1) said inherit_label needed a "profile" case; its else-branch already handled a profile win, and only naming the tier was missing; (2) said _announce_many does one joined read; it runs five statements, and the profile is read in the hydrate step; (3) left re-arm as an open question without naming the hazard that re-saving a fired profile with past dates fires it again (the implementer's 422); (4) missed that resolved_interval (String(32)) and Watcher's timedelta overflow on a very long interval (CR 8); (5) again didn't flag the docs' context budget: five docs over 10k needed a curation pass (repeat of Watch lifecycle: archive and restore #361's error 5); (6) post-merge step 2 assumed the deploy enables the new timer; it installs only, and needs enable --now; (7) didn't weigh audit_stored_documents for rules (exempted, with a reason).
  • Notification subscriptions: model, API and UI (global/domain/item scopes) #363 Notification subscriptions: model, API and UI (global/domain/item scopes) · M · PR #363 Notification subscriptions: model, API, UI and the Watcher import #446 (fa9ee65), merged 2026-10-28T22:55:53Z; D1–D6 taken, plus the implementer's remote_channel_id NOT NULL (a null channel refuses on import). CI green by job on 8b7c41d and fa9ee65 (a Dependabot uv run on fa9ee65 failed: Dependabot security updates for uv fail on every advisory (co-core 404) and mark main red #437, unrelated). Live fa9ee65960ee = /health = main since 2026-10-28T23:04:31Z (deployed by the implementer with the operator's authorization [stated]); OpenAPI 4.39.0, SDK 5.16.0; alembic b7e3c95a1f20 → c4d81f6a2e07; no warnings since deploy; drift ok; bus-health 0 findings [measured]. Import done (issuecomment-6483086496): 2 global subscriptions, both active on all three events, imported_watcher_id set, channels mapped to 01M5AX8CE8PBMQZMQ5TBBB9RAP (Slack) and 01M5AX8CEAEKPM7JJ3D0VY20AF (Mailgun), no Jinja, no overrides [measured]; a re-run landed on the same ids [stated]. No subscription write reached the outbox; the only rows since deploy are two source_revision_captured pairs (2026-10-29 16:23Z, 21:28Z) [measured]. 8001 checks passed (issuecomment-6483149901) [stated]. Coordinator errors in the brief: (1) D1 listed remote_channel_id without saying Watcher's is nullable or ruling on it; (2) gave no lock rule for the refs: a create racing a Domain or item delete hit the FK and 500'd (CR 1, now FOR KEY SHARE), the Resolve cadence and announce a concrete interval #364 class again; (3) D3's whole-document PATCH plus D5's hidden Jinja fields meant a dashboard toggle edit would erase an API-set template; the implementer caught it in review; (4) again didn't weigh audit_stored_documents (repeat of Temporal profiles: model, API/UI, daily boundary job, post-actions #365's error 7): the new sweep kind crashed /dashboard/registry-audit, which domain_watch_spec had done unseen since Domains: become the sole owner (min_interval, default cadence, deactivation) #360 (CR 7); (5) traced both matchers but didn't flag that Watcher matches on its item's own domain_name while Archiver uses the announcing source's host, the first place to look on a info.watch-events consumer: shadow → dispatch #369 parity miss; (6) missed that the SDK body model silently drops unknown keys (update(id, {"scope": …}) answered 200 unchanged); (7) missed SCREENS.md Dashboard: Domain detail — align with detail-screen conventions #82's group-count rule (CR 4).
  • Notifier credential behind a unit-only enable flag #368 Notifier credential behind a unit-only enable flag · S · PR #368 feat: Notifier credential behind a unit-only enable flag #450 (5cfb5da), merged 2026-10-30T04:49:50Z; D1–D5 taken, plus two operator rulings from the implementer review (issuecomment-6511197118): the page is /dashboard/settings/notifier, and the gate refuses an environment key aimed at :9000. CI green by job on 4018cfa and 5cfb5da (test re-run green after Notification subscriptions: model, API and UI (global/domain/item scopes) #363's tombstone flake, fixed by test: pick the cascade tombstone by generation, not row order #455; Dependabot uv failure is Dependabot security updates for uv fail on every advisory (co-core 404) and mark main red #437). Live 5cfb5dacbe27 = /health since 2026-10-30T14:58:57Z, NRestarts 0; main is 2 commits ahead, test and submodule only (drift ok, "none that runs"); OpenAPI 4.39.0, alembic c4d81f6a2e07 unchanged; no warnings since deploy; bus-health 0 findings [measured]. Journal: Notifier configured at http://notifier:9000 (key: credential), then exactly one Notifier call, GET /api/v1/channels 200 at 15:49:41Z from the operator's page load [measured]; key 0 times in the journal [stated]. Outbox unchanged: persist 28, replicate 3, changes 25, registry 3 [measured]. 8001 listed dev-sink/dev-sink-failing with the real dev key [stated]. Coordinator errors in the brief: (1) D4 named /dashboard/settings, a 404 (Settings is only /dashboard/settings/api-keys); (2) D3 had the parity test mirror a :9000 refusal D2's gate didn't contain; (3) gave v0.3.2 as f3efc68, the annotated tag object, not the commit 8e7c020; (4) traced the generated API but missed the SDK's channels facade, which also exposes send_test and channel writes, so the brief named no single-importer adapter or guard on them; (5) D4 promised a channel type ChannelOut doesn't carry (shown from the masked URL's scheme); (6) flagged Dependabot as a risk, but it tracks github-actions only; (7) named the ordering hazard but not the fix: deploy.sh now refuses a missing no-fallback credential before CI (CR 4), the deploy-time host-prerequisite class again.
  • Notification composer, preview and test-send #367 Notification composer, preview and test-send · M · unblocked (Notification subscriptions: model, API and UI (global/domain/item scopes) #363 live 2026-10-28). CannObserv/cannobserv#568 is cleared. From Notification subscriptions: model, API and UI (global/domain/item scopes) #363: render in SandboxedEnvironment; put the Jinja fields beside the preview and drop the form's carry-over; test-send to a production channel is an operator action; include_tags inert until InfoItem tags #362. From Notifier credential behind a unit-only enable flag #368: src/core/notifier/ is the only notifier_client importer and exposes list_channels alone, with a guard test keeping send_test, test_channel, dispatch and channel writes out of src/; test-send widens both on purpose. Channel names come from /dashboard/settings/notifier's call (one attempt, 5 s); ChannelOut has no type field. · PR #367 feat: notification composer, preview and test-send (epic #359) #456 (ea53550), merged 2026-10-30T21:56Z; D1–D7 taken, Q1 answered (a): ARCHIVER_PUBLIC_BASE_URL is the origin, and the API's dashboard_url was fixed to /dashboard/info-items/{id} (v4.40.0, [service] entry; SDK unchanged 5.16.0). CI green by job on 4b1e03f and ea53550 (6 Dependabot uv failures on the merge SHA are Dependabot security updates for uv fail on every advisory (co-core 404) and mark main red #437). Live ea5355088791 = /health = main since 2026-10-31T00:12:28Z, NRestarts 0; OpenAPI 4.40.0; alembic c4d81f6a2e07 unchanged; no WARNING since deploy; drift ok; bus-health 0 findings [measured]. Journal: text reader configured (bucket co-gcs-processor), links at https://co-registrar.exe.xyz, Notifier configured; no Notifier call since deploy [measured]. Outbox: 0 rows created since deploy, max ids unchanged; counts fell only by the hourly changes_outbox grows without bound (no pruner of published rows) #189 prune of rows older than ~25 days [measured]. Host step 1 done by the implementer with the operator's authorization [stated]: the base-URL line removed from /etc/archiver/.env (the unit now carries it); SA archiver-processor-text-reader key installed. 8001: previews and a test-send to dev-sink (dispatch 01M672JR9QV071539XKXKGPC4D) [stated]. Coordinator errors in the brief: (1) D2's link didn't reconcile with the existing dashboard_url ({base}/info-items/{id}), so the meaning of ARCHIVER_PUBLIC_BASE_URL had to go to the operator (Q1); production's dashboard_url had been a 404 all along, which the brief missed; (2) predicted no contract path; the fix needed a CHANGELOG entry; (3) D1 gave no Archiver source for last_changed_at or the interval, and missed the previously_notified_at input Watcher's error body needs for reminders; (4) recommended the sandbox without bounding its resource use (*/**) or rendering off the loop (CR 1); a per-render budget is still open for info.watch-events consumer: shadow → dispatch #369; (5) step 4's proof leaned on Watcher's process_commands, though the processor's keys are content-addressed and Archiver holds candidate fingerprints.
  • info.watch-events consumer: shadow → dispatch #369 info.watch-events consumer: shadow, then dispatch · M · unblocked (Notification composer, preview and test-send #367 live 2026-10-31, Notifier credential behind a unit-only enable flag #368 live 2026-10-30). Unblocks F3: notification cutover instant T; stop dispatching watcher#376 (F3). Carries TRIAGE_DLQS pin: three queues once archiver.watch-events consumes (broker#93 granted +xdel on info.watch-events.dlq) #399. Create the group at 0. From Notifier credential behind a unit-only enable flag #368: only archiver.service holds the credential, and a units test pins that no other unit carries notifier config; a consumer in its own unit needs its own LoadCredential and that test changed. The lifespan builds the client last, one per process. From Notification composer, preview and test-send #367: dispatch only via Notifier.dispatch_composed (D3); load the diff with load_change_diff(app.state.text_reader, event) only when diff_requested(options); feed previously_notified_at for reminders. Open: a render budget against nested-loop CPU in saved templates. Default text is Watcher's word for word, [Watcher] title prefix included, for shadow parity; decide the prefix at F3. · Briefed 2026-11-01 (issuecomment-6564279582), D1–D8. Recommended: D1 one table notification_dispatches (shadow log + dispatch outbox), unique (event_id, subscription_id), composed at consume; D2 group at 0 under the revisions gates, check_rejected ignored, TRIAGE_DLQS pin: three queues once archiver.watch-events consumes (broker#93 granted +xdel on info.watch-events.dlq) #399's three DLQs; D3 ARCHIVER_NOTIFY_CUTOVER_AT unit-only, send only when set and occurred_at ≥ T; D4 drain retries transport errors on the same key, any DispatchOut terminal (Notifier's key returns failed records too), no redeliver; D5 reminders by timer from the recorded episode, active/unarchived/unsuspended items only; D6 parity by evidence join (dashboard log + Watcher's audit export; told F3: notification cutover instant T; stop dispatching watcher#376 issuecomment-6564281863); D7 render caps (range 1 000, 64 KiB); D8 docs (ARCHITECTURE 48 tokens).
  • Check-now via info.watch-check #370 Check-now via info.watch-check · S · unblocked. Unblocks F4 (no watcher issue).
  • Watch panel: last_full_fetch_at and health reason #371 Watch panel: last_full_fetch_at and health reason · S · unblocked; rewrites test_co_core_037_status_fields_parse_and_are_ignored.
  • InfoItem tags #362 InfoItem tags · S · unblocked; the composer reads them.

Phase 3 — the jump

Phase 4 — hygiene

After F3

Order

The design doc's cutover phases (Phase 1 build dark → Phase 2 flips F1 → F2 → F3 → F4 → Phase 3 jump → Phase 4 hygiene), plus each line's blocked-by edges.

Tiebreaks the coordinator added, numbered so the operator can challenge one:

  1. Flip order: a child on an earlier flip's path goes first (F1 → F2 → F3 → F4), because Watcher flips one at a time in that order.
  2. Dependency before size.
  3. Context freshness: the child whose code just changed goes next.

Overrides: 2026-10-24: the operator took this epic into the #357 coordinator thread, ahead of that epic's queue, because Watcher's flips all wait here.

Boundary decisions

Exit criterion

Every child closed and verified live, and Watcher's F1–F4 flipped (read on CannObserv/watcher#368).

Verify recipe

  • Gate: CI on the final SHA and on main's tip, by job: test, lint, changelog, client-drift (and uv audit (report-only), which never fails).
  • Deploy: readlink /srv/archiver/live equals /health's build_id equals main. Runtime paths merged without a deploy raise drift lag after 8 h.
  • By deliverable: migration → alembic_version via psql -d archiver (read-only); route → the served /openapi.json; stream → the outbox and the broker grant; consumer → its group on the broker and bus-health; dashboard → the operator re-checks on production.
  • Production writes (imports from the Watcher export, flips) are the operator's, each time.
  • Baseline: CI green by job on 319405a, 2026-10-24.

Waits

Ref Who waits On what Asked Unblock condition
CannObserv/watcher#375 Watcher (F2) #364 and #365 same both live (2026-10-28); operator's go given 2026-10-28, Watcher-only flip before the 2026-11-01T23:00Z freeze (CannObserv/watcher#375 issuecomment-6475937810). Archiver's one duty: rerun the D5 dry run if the pre-flip re-export moved beyond exported_at
CannObserv/watcher#376 Watcher (F3) #369 shadow parity same parity gate met, plus the operator's go
F4 (no watcher issue) Watcher #370 same live, plus the operator's go
CannObserv/broker#95 broker F1 done (the jump) its body Watcher's content.fetch-policy grant revoked after F1

Verification debt

What is unverified Why it was skipped How to discharge it Discharged by
F1: no ignoring a fetch policy without a generation… lines on co-replicator after 2026-10-26T18:57:22Z; all 4 hosts hold ordered frames archiver can't read Replicator's journal Replicator's answer on CannObserv/replicator#135 CannObserv/replicator#135 issuecomment-6428523297 (22:10Z): 0 such lines; 0 older than refusals; ordered frames inferred (no new applied host) [stated]
F1: broker's content.fetch-policy probe is clear apart from the 18:55Z–21:42Z gap's age WARN archiver holds +xadd only broker's answer on CannObserv/broker#95 CannObserv/broker#95 issuecomment-6429279923: only the age WARN (19:10Z–21:37Z), 0 findings from 21:48Z; no generation: null entry after 18:55:01Z [stated]
F1 step 2's --dry-run read-back traced instead of run the next re-import's --dry-run shows nothing pending #364 host step 4 (2026-10-27): 4 unchanged, no differs [stated]
A content.fetch-policy delta through the outbox in production no domain edit since F1 (still 0 rows on 2026-10-27) changes_outbox rows on the topic drain (published_at set) after the next domain edit (open)
Watcher's F2 thread doesn't yet have the implementer's F2 notes (issuecomment-6429075588) GH_TOKEN_WATCHER returns 401 since 2026-10-27 post them on CannObserv/watcher#375, or hand them to Watcher's coordinator (CannObserv/watcher#394 thread) posted 2026-10-27 on CannObserv/watcher#375 (issuecomment-6447604909) after the token was rotated
The first scheduled archiver-temporal-boundary run (2026-10-29T00:05Z) the timer was enabled 16:20Z on 2026-10-28; only a hand run so far (evaluated 0, announced 0, Result=success) systemctl list-timers shows LAST passed; the journal says temporal boundary finished with no WARNING; info.registry outbox still 3 while 0 profiles exist coordinator 2026-10-29: ran 00:05:17Z, today 2026-10-29, evaluated/moved/fired/announced/skipped 0, no WARNING; info.registry still 3 [measured]
#368: Notifier records 0 dispatches for tenant archiver since 14:58:57Z, and archiver-prod has a fresh last_used_at from the 15:49:41Z page load archiver can't read Notifier's database Notifier's answer on CannObserv/notifier#116 (asked issuecomment-6527020944) CannObserv/notifier#116 issuecomment-6528964636: 0 dispatches ever; last_used_at 15:49:41.28Z; the only co-registrar request since deploy is GET /api/v1/channels 200 [stated, Notifier measured]
The processor-text read (from #367) proven end to end (ok <bytes> through the unit's credential) needs a real stored text; only a clean 404 on a missing object so far [stated] operator runs NOTIFICATION_COMPOSER.md § Proving the read with a real pair (candidate: an Archiver content_fingerprint, or one from CannObserv/watcher#368) operator 2026-11-01: ok 17735 on the live release with the newest Archiver content_fingerprint [stated]; so Archiver's fingerprints are the processor's keys
Notifier (for #367) shows 0 archiver dispatches since 00:12:28Z (bar operator tests), and the dev dispatch carries {{ title }}/{{ body }} with the text in variables archiver can't read Notifier's database Notifier's answer on CannObserv/notifier#116 (asked issuecomment-6563850655) (open)

Log

Activity

  1. gregoryfoster commented on Oct 17, 2026

    @gregoryfoster
    ContributorAuthor

    cannobserv v0.37.0 is published (tag, release commit 49d42aca, Publish run green with 4 wheels + 4 sdists; CHANGELOG). It is a MINOR, but every entry is additive: the owner chose MINOR to mark the headless-Watcher contract set (CannObserv/cannobserv#568). The sibling floors are >=0.37,<0.38, so a pin <0.37 (or ==0.32.0) needs an explicit bump to pick it up.

    The headless-Watcher contracts Phase 1 builds against are released. Pointers for this epic's children that aren't tracked elsewhere (#371, #366 and #360 each have their own pointer):

    • Consume info.watch-events in group archiver.watch-events. Delivery is at-least-once, so dedupe on the envelope key, which is event_id for all four facts. They share one key namespace on the stream.
    • Issue WatchCheckCommandEmit on info.watch-check for check-now. There is no ack. A rejection closes the command by its command_id field on a CheckRejectedEvent. Otherwise the outcome is on info.watch-status, which names no command: last_full_fetch_at passing the command's requested_at, plus health.
    • Close an open error episode on the item's revoke. Treat a repeat error edge for an item that's already in error as superseding the open episode: the error edge is any health into error, not only ok → error.
    • Re-baselines, Announce a renewed blob horizon for an unchanged fingerprint: archiver's stored blob_expires_at freezes at first observation (archiver#201, producer half) watcher#293 renewals and repeated failures are not events.

    The pair doc is docs/change_bus/watch.md. Archiver main (d07144c) pins co-core>=0.32,<0.33.

  2. gregoryfoster commented on Oct 19, 2026

    @gregoryfoster
    ContributorAuthor

    Cross-epic note from the epic #357 coordinator (2026-10-19): CannObserv/cannobserv#568 is closed, and its models shipped in co-core 0.37.0/0.37.1. Archiver is still on co-core 0.32, though (Processor and Watcher are on 0.37.0). #397 is the catch-up, and it is a prerequisite for #371 and #370, and later #366 and #369, as well as for #357's #394. #357 will slot #397 next unless the operator says otherwise. If this epic's thread lands it first, #357 only needs to know.

  3. gregoryfoster commented on Oct 19, 2026

    @gregoryfoster
    ContributorAuthor

    Cross-epic, from the epic #357 coordinator, 2026-10-19: #397 is live. Archiver is on co-core / co-core-aio 0.37.1 (PR #398; live 246db8a11f75 = main) [measured].

  4. gregoryfoster commented on Oct 19, 2026

    @gregoryfoster
    ContributorAuthor

    Broker grants for the headless-Watcher work are live (CannObserv/broker#93)

    Live 2026-10-19T17:46:17Z (broker server time), applied as acladmin with ACL SAVE. Shipped in CannObserv/broker 93474fc..4c61a43. ACL LOG recorded no denial. (Your four connections reconnected at about 17:45:29Z, 48 s before the apply; that was on your side, not caused by the apply.)

    Archiver's line now (selectors only; the root is +info +ping):

    (+xreadgroup +xack +xautoclaim +xgroup|create ~content.revisions ~content.artifacts ~info.watch-events)
    (+xpending ~content.revisions ~content.artifacts)
    (+xread ~info.watch-status)
    (+xlen +xrange ~content.revisions.dlq ~content.artifacts.dlq ~info.watch-events.dlq)
    (+xadd ~content.replicate ~content.persist ~info.changes ~info.registry ~content.revisions.dlq ~content.artifacts.dlq ~content.fetch-policy ~info.watch-check ~info.watch-events.dlq)
    (+xtrim ~info.changes)
    (+xdel ~content.revisions.dlq ~content.artifacts.dlq ~info.watch-events.dlq)
    

    For #369 (consume info.watch-events):

    • Group archiver.watch-events, through your consume selector. Create it at 0, not $. Watcher's outbox (watcher#369) deploys first, and its facts accumulate ungrouped until your consumer runs, so a $ create skips every one of them. This is the standing rule: never $ against a stream that holds entries.
    • Broker's probe treats the group as dormant until seen: silent until it first appears, then group-missing every tick if it goes away. Recreating at 0, as your other groups do, is the right recovery.
    • +xpending does not reach this stream. It moved into a selector of its own on content.revisions and content.artifacts. The behaviour there is unchanged, and the dashboard lag read keeps working. If the dashboard should read archiver.watch-events too, ask for it on a broker issue.
    • info.watch-events.dlq is yours to write (dead_letter), audit (XLEN/XRANGE) and dispose of by XDEL. There is no trim on it. Your test_triage_dlqs_are_the_two_queues_broker_grants_xdel_on pins two queues, and broker's +xdel selector now names three. When info.watch-events consumer: shadow → dispatch #369's group joins TRIAGE_DLQS, that pin moves to three. Broker's test_archiver_dead_letter_disposals_name_their_caller is held to the three it drains.
    • The undelivered-age threshold is the shared 300 s, on the assumption of a blocking XREADGROUP through co-core-aio's driver. If shadow mode reads differently, say so.

    For #370 (check-now via info.watch-check): +xadd only, content.persist's shape. Archiver has no read, no group command and no trim on the stream, and no key on its DLQ, which is Watcher's. Watcher's group exists before your first command (watcher#371 runs live in Phase 1), and a command written without it is group-missing on broker's probe.

    For #366 (content.fetch-policy, dark until F1): +xadd is live. The cap is the MAXLEN on each publish, which needs no trim grant, so none is given. Watcher holds +xadd until CannObserv/broker#95. D8 (one producer at a time) is the flip's rule and no ACL can enforce it. Broker's probe does not see two writers at today's set sizes (replayed: silent at 3+4 and 30+30; from 60+60 up, a standing stream-length that names a second producer as a possible cause). Replicator's journal is where dual-run shows (replicator#135). At F1 the mirrored 500 and 10-set floor become yours: CannObserv/broker#94.

    For #397 (co-core catch-up to 0.37.1): broker is pinned co-core>=0.37.0,<0.37.1. 0.37.1's content.capture has no broker row, group or grant, and broker registers a stream only on a participant's named request. When archiver#380 or replicator#138 is ready to publish or consume it, that request lifts broker's pin. Your own bump is independent of this.

  5. gregoryfoster commented on Oct 20, 2026

    @gregoryfoster
    ContributorAuthor

    Pointer from epic #357's coordinator (2026-10-20): #399 (the TRIAGE_DLQS pin goes to three queues) belongs in #369's PR. #369 is what adds archiver.watch-events to OWNED_GROUPS. When that happens, info.watch-events.dlq appears on the Bus triage screen (#271, live) by itself. Two conditions from #271: the existing test requires a reprocessor for the new queue, and the broker +xdel grant (CannObserv/broker#93) is already in place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions