Skip to content

[Epic] content.fetch-policy producer moves to Archiver (parked until F1) #135

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

Order

The design doc's cutover phases, plus each line's blocked-by edges (inherited from CannObserv/watcher#368).

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

  1. Unblocked before blocked.
  2. A multi-minor co-core pin move lands alone, in an isolated venv, before any behaviour that needs the new version (the Converge storage on the cluster content-addressed model #114 rule). Catch up co-core 0.19.6 → 0.36.0 ahead of the generation release #140 was that move. Apply FetchPolicyState.generation only if newer #136's one-minor bump rides with its rule, because generation is the only change it brings to FetchPolicyState.
  3. Context freshness: the child whose surface just changed goes next.

Overrides:

Exit criterion

Every child is closed and verified live before F1 (CannObserv/watcher#374) starts: the production worker runs a co-core carrying FetchPolicyState.generation and applies it per CannObserv/cannobserv#566's rule, and docs/contracts/replicator-boundaries.md names the producer change and links cannobserv's docs/BOUNDARIES.md as its parent. F1 itself belongs to the parent epic.

Met 2026-10-19. Decided 2026-10-19 (operator): park, don't close. The epic stays open as a parking lot for #144 until F1.

Verify recipe

  • Gate: CI by job (test, lint, gcs (T4 against the test bucket)) on the merge SHA and on main's tip (run at adoption: all green on f638266).
  • Deploy: the main checkout is on main, clean, at origin/main. When runtime code or a dependency changed, replicator.service ActiveEnterTimestamp is after the merge, and the boot journal shows the policy replay summary (tracked_hosts) with no decode warnings.
  • Baseline: uv run pytest in the main checkout, no env file sourced: 1408 passed, 1 skipped, 71 deselected (integration), coverage 99.89%, 2026-10-17.
  • By deliverable: co-core pin → uv.lock and uv pip show co-core in the main venv; consumer rule → its tests plus the replay journal line; contract doc → tests/test_boundaries.py green and no dead links.

Issues

Phase 1 — build additive and dark

Waits

None held by this epic. F1 is live: Watcher's producer stopped 2026-10-26T18:57:22Z (CannObserv/watcher#374 part 1; the issue is still open), and Archiver's started 21:42:27Z (CannObserv/archiver#438; CannObserv/archiver#366 closed 2026-10-26).

Parked

Ref Re-entry trigger Last re-read
#144 After F1: state the fetch-policy producer as Archiver, retire the "until F1" wording Unparked 2026-10-26 (operator). The trigger is met in substance: Archiver's producer has been live since 21:42:27Z with a clean read-back, and CannObserv/watcher#374 is open only for its part 2, which is Watcher-internal cleanup that changes nothing Replicator's docs say. Its record stays on this epic until close; #154 only orders it (after #148) through its Parked row, not as a checklist line. Before the brief: re-run the 2026-10-19 pre-flight against origin/main (sites, sizes, and the unordered-path question against CannObserv/cannobserv#579) 2026-10-26

Measured state

Rewritten from scratch at each check-in, never appended to.

  • Production runs build 66321b5 on co-core 0.39.1, booted 2026-10-26T18:55:53Z. The boot replay applied 500 messages, tracked_hosts 4, hosts_stricter_than_default 0. Since then there are 0 ignoring a fetch policy lines of either kind. Measured 2026-10-26T22:07Z.
  • docs/contracts/replicator-boundaries.md is 9838 of 10000 and AGENTS.md 5989 of 6000, per [Epic] Backlog burn-down: live stability, sibling waits, operational conformance (2026-10) #154's Measured state of 2026-10-26.
  • CannObserv/cannobserv#579 (require generation on emit) is still open, last updated 2026-10-17.

Related, not in this epic

Verification debt

What is unverified Why it was skipped How to discharge it Discharged by
A fetch, replicate or persist outcome on the new co-core no command arrived after the 0.36.0 restart the journal shows an outcome after worker ready 2026-10-18: 15 blobs stored on 0.36.0, 18 on 0.37.0
The ordered paths against real Archiver frames no Archiver producer until F1 the journal after F1, per #144's pre-flight 2026-10-26, the read-back #135 (comment): 0 unordered frames after the stop, 0 ordered refusals, 0 new hosts applied, 0 poll failures; with fetch_policy_supersedes, every host holds an ordered frame

Overlaps

None open.

Log

Activity

  1. changed the title [-][Epic] content.fetch-policy producer moves to Archiver[/-] [+][Epic] content.fetch-policy producer moves to Archiver (parked until F1)[/+] on Oct 19, 2026
  2. gregoryfoster commented on Oct 19, 2026

    @gregoryfoster
    ContributorAuthor

    Parked until F1 (coordinator, 2026-10-19)

    The exit criterion is met. All four children are verified live, ahead of F1:

    Live state left behind:

    • Production runs build e3cb04f on co-core 0.37.0. main is at 1c53ea4, which adds docs only since that build.
    • The fetch-policy replay shows 4 hosts and no "ignoring" lines.
    • AGENTS.md is at 5991 of 6000 tokens exact. The boundaries doc is at 9726 of 10000.

    Parked: #144, the post-F1 docs cleanup. It also carries the moved verification debt: the ordered paths have not yet run against real Archiver frames. Its trigger is CannObserv/watcher#374 closed and CannObserv/archiver#366's producer live. The pre-flight is on the issue.

    This thread stays the coordinator for #144. Implementation stays in separate threads.

  3. gregoryfoster commented on Oct 19, 2026

    @gregoryfoster
    ContributorAuthor

    Process-log draft, held until this epic closes (operator decision 2026-10-19). At close: update the dates, add the F1 check-in, then file this on gregoryfoster/skills with GH_TOKEN_SKILLS, as the entry process-log/2026/<close-date>-replicator.md plus one index row. This draft names no identifier from the private contracts repo, because the log is public.

    Entry draft

    Session 2026-10-19 — #135

    A child epic of CannObserv/watcher#368 (headless Watcher), adopted three days after its creation and parked once its exit criterion was met. Replicator is a bus worker with no HTTP surface. Its deploy is the main checkout plus a service restart. The run made Replicator's consumer of a config/state stream ready for a producer change that happens in a sibling repo. The shared contracts library lives in a private repo, so it is named here by its role only: "the contracts repo" and "the library".

    Shape

    • Way in: inherited, then adopted. The epic was filed from a cross-repo design doc's issue breakdown, with two children and no cursors. Adoption added the template sections in one call. scan.sh had no cursors on its first run, so the watermark and since were passed by hand from the epic's creation.
    • Children: 2 at creation, 4 at the end. One was filed at adoption on the operator's decision. One was a review-born follow-up, which the operator bundled into an existing child. One post-flip follow-up was filed by the last implementer and parked.
    • Check-ins: adoption, 3 check-ins, and one reorder between check-ins. All four children shipped and were deployed between 2026-10-17 and 2026-10-19.
    • Baseline: 1408 passed and 1 skipped at adoption, 1421 and 1 at the end. The skip set didn't change.

    What was different

    • A large version jump was hidden in a small child. The behaviour child needed the library's next release, and the host pinned a minor version 17 releases behind it. The library's local mirror was also stale: two releases behind the index. The coordinator found this only by comparing the pin to the index during adoption. The issue said nothing about it. It became its own child, unblocked and ordered first, while the release it waited for was still being cut.
    • The upstream helper's rule differed from local behaviour. The library shipped a decision function for the new ordering token. For two frames without the token, it answered "stream order". The host's consumer also dropped a frame stamped older than the one it held, and a test pinned that. Calling the helper alone would have changed behaviour for every frame before the flip. The brief made it an operator decision: keep the local guard for frames without the token, and let the token decide alone once present. Starting the thread approved it.
    • A release reordered the queue. Once the release published, the behaviour child moved ahead of the docs child. Landing it first let the docs child describe the consumer as shipped, not as planned. One brief was edited in place and the other written.
    • The epic parked rather than closed. The exit criterion was met before the sibling's flip, but the flip leaves a Replicator-side cleanup and a check against real frames. The operator chose parking-lot mode, with an AND trigger: the flip issue closed, and the new producer live.

    Findings

    Adoption

    • Compare the host's pin to the index, not to the issue, whenever a child needs an upstream release. "Patch bump plus a rule" was really a 17-minor jump.
    • Sync the local mirror before measuring version distance. A stale mirror under-reported the gap by two releases.
    • A child that needs a release can be split: a catch-up to the newest published version, which is unblocked now, then a one-minor bump with the behaviour. The catch-up absorbs the risk while the release is still being cut.

    Harvest

    • A note harvested from an upstream plan goes stale when the plan ships. Two of the planned rule's cases didn't ship. The coordinator marked its own note as corrected, in place and visibly, and posted the shipped rule below it, quoted from the released wheel.
    • Read the released artifact, not the report. Unpacking the wheel showed the helper's exact branch for two unordered frames, and that settled the brief's one decision.
    • An upstream coordinator's "pointer posted" claim can be wrong. The release comment listed a pointer on the host child that never arrived. The content was already there from the harvest, so the gap cost nothing. It was reported, not chased.

    Verify

    • "Not deployed yet" in a report can be stale within minutes. The service had restarted after the report was written. Read the unit's start time and build stamp, not the comment.
    • Discharge "no traffic yet" debt at the next check-in from the journal. Count outcomes per build window, and attribute a worker stopped line by its timestamp to the outgoing instance.
    • A multi-commit push builds only its tip. An implementer's claim that CI runs on every commit didn't hold. The tip was green, so this was noted, not blocking.
    • The coordinator ran one approved host step itself: the code-index refresh that the implementer's run had skipped on another session's lock. It is non-code and idempotent, and the host steps called for it.

    Ordering and budget

    Ending

    Parked 2026-10-19. One parked item, with a brief-grade pre-flight on the issue: the verification debt moved from the epic, the re-check list, and an open question about when the unordered path can be retired. The title says "parked until F1", and Next is none.

    Index row draft:

    | 2026-10-19 | #135 | An inherited child epic, adopted with no cursors: 2 children grew to 4 over 3 check-ins in 3 days, then parked for a sibling flip. A 17-minor pin jump hid inside a "patch bump" child and became its own child. A released helper's rule differed from local behaviour; the operator decided. A release reordered the queue |

  4. gregoryfoster commented on Oct 26, 2026

    @gregoryfoster
    ContributorAuthor

    F1 is live: Archiver produces content.fetch-policy (from archiver's epic coordinator, CannObserv/archiver#359, 2026-10-26).

  5. gregoryfoster commented on Oct 26, 2026

    @gregoryfoster
    ContributorAuthor

    Hand-off: this epic's coordination moves to #154's coordinator (operator, 2026-10-26)

    From here on, the thread coordinating #154 also owns this epic. This comment is everything you need, in the order it's due. The body is the state; this comment is the transfer.

    Where it stands. All four children were verified live before F1, and the exit criterion was met on 2026-10-19. The epic is open only as the parking lot for #144. F1 is now live (#135 (comment)):

    Owed now

    1. The read-back Archiver's coordinator asked for (22:03Z), not yet answered. I took the readings below at 22:07Z, from journalctl -u replicator only, for build 66321b5, booted 18:55:53Z. They're ready to post as the reply on this epic.

    • 0 ignoring a fetch policy without a generation for a host that holds one lines after 18:57:22Z. No unordered (Watcher) frame arrived after the stop.
    • 0 ignoring a fetch policy older than the one already applied lines since the 18:55:56Z boot, a window that covers all of Archiver's sets.
    • "Each of the 4 hosts holds an ordered frame" has no direct journal line, by design.
      • src/worker/policy.py logs applied a host fetch policy only when a host's interval changes (Fetch-policy tail re-reads a static stream 11-32x/sec, logs ~31 lines/sec, and the two group consumers hold no connection #85). Archiver's intervals equal the held ones, so an applied frame is silent. An equal generation is also silent, because that's every republish's steady state.
      • The inference [from the code]: each host held Watcher's unordered frame (generation None). If a host's first Archiver frame had been refused, every later set for it would differ from the held generation, and each would log the "older than" line. There are 0 such lines across 5 or more sets, so every host's first ordered frame applied.
      • Proving it directly would take a restart, and nothing else calls for one.
    • A correction to "ran on its conservative default" for the gap (18:55 to 21:42Z).
      • The worker kept the frames its 18:55:56Z boot replay applied: 500 messages, tracked_hosts 4, each at min_interval_seconds 1.0, hosts_stricter_than_default 0. Nothing tombstoned them, so it ran on Watcher's last values, not the default.
      • Those values equal the 1.0 s default, so pacing didn't change either way.

    2. Decide whether #144 unparks (operator).

    3. #144's brief. Its pre-flight is #144 (comment) (2026-10-19). Re-run it against origin/main before posting the brief:

    4. At close, after #144 is closed and verified:

    Mechanics

  6. gregoryfoster commented on Oct 26, 2026

    @gregoryfoster
    ContributorAuthor

    Read-back for F1 (coordinator, now for this epic too, 2026-10-26T22:10Z)

    This answers Archiver's coordinator. It comes from journalctl -u replicator, build 66321b5, booted 18:55:53Z, NRestarts 0, read at 22:09:48Z. It re-checks the hand-off's readings, which hold.

    1. No unordered frame after Watcher's stop [measured]. There are 0 ignoring a fetch policy without a generation for a host that holds one lines after 18:57:22Z.

    2. Each of the 4 hosts holds an ordered frame. This is inferred, not logged, but the inference is tight.

    Reading [measured] What it rules out [traced]
    0 ignoring a fetch policy older than the one already applied lines since boot. A first Archiver frame refused. co-core 0.39.1's fetch_policy_supersedes returns true for any ordered frame over an unordered held one (stored.generation is None or incoming.generation > stored.generation), so that frame can't be refused. A later refusal of a different generation would log this line.
    0 applied a host fetch policy lines after the replay summary (18:55:56Z). A host Archiver publishes that the worker didn't already hold. A new host has no previous interval, so it always logs applied (src/worker/policy.py, _store). So Archiver's 4 hosts are the 4 the boot replay held: app.leg.wa.gov, governor.wa.gov, lcb.wa.gov and tailscale.com. Each is at 1.0 s, equal to the held value, which is why applying it is silent (#85).
    0 fetch policy poll cycle failed and 0 WARNING or ERROR lines since boot. A tail reader that stopped reading.

    So every host's first Archiver frame applied, and later equal-generation republishes are skipped silently, as designed. A direct proof would need a restart and a replay, and nothing else calls for one.

    3. A correction to "Replicator ran on its conservative default from 18:55Z to 21:42Z." It didn't. It kept the frames its 18:55:56Z boot replay applied: 500 messages, tracked_hosts 4, each at min_interval_seconds 1.0, hosts_stricter_than_default 0. Nothing tombstoned them, so through the gap it ran on Watcher's last values. Those equal the 1.0 s default, so pacing was the same either way.

    What stays: the occurred_at guard for frames without a generation, until CannObserv/cannobserv#579 makes generation required. It's open, and an Archiver frame built without one is still accepted by co-core's model. #144's brief will settle that.

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