The previous round (88f0923) threaded a `backend_id` from `BACKEND_ID`/`HOSTNAME`
env vars into `AppState`. That required the operator to manually align each
instance's `BACKEND_ID` with the Cloudflare Worker's `BACKEND_ORIGINS_JSON`, which
is exactly the kind of manual cross-config sync that drifts.
The request's own `Host` header already carries the answer: the Worker load
balancer (`ARCH-LB-WORKER`) preserves the inbound `Host` while rewriting the target
to a backend origin (`joinUrl` builds `origin + path + query`), so `Host` is the
public address the LB routed this instance as. `backend_label_from_headers` reads
it per-request in `setup_status`, trims the default https/http port
(`example.com:443` -> `example.com`) so the label mirrors a bare origin, and returns
`None` when the request carries no usable `Host`.
This removes the startup env entirely — `backend_id_from_env`, `AppState::backend_id`,
all five router-builder `backend_id` parameters, the `.env.example` `BACKEND_ID`
entry, and the `BACKEND_ID` env documentation are all gone. Handlers still never
touch the environment: `Host` is request-scoped, not process-global, so there is no
test race (the `debug_surface_requested` rationale was about env, not headers).
Tests: `setup_status_reports_backend_label` now sends `Host: edge-1.example.com:443`
and asserts the label is the bare origin `edge-1.example.com` (port trimmed). A new
`setup_status_reports_null_backend_without_host` asserts `null` when no Host is sent.
GATE-P0 106 passed / 0 failed / 4 ignored; GATE-DOCS 6/6 clean (49 line-refs).
Sync the stated-as-current gate numbers that the previous feature commit
(88f0923) shifted, per the same-round rule:
- Test baseline 104 -> 105 passed / 0 failed / 4 ignored across the
authoritative (charter §6.1) and deferring docs (design §Phase 0,
opengaps access-control, HANDOFF-push-401), with retest date 2026-10-14.
The new test `setup_status_reports_backend_label` is a normal (non-ignored)
test, so the ignored set is unchanged.
- opengaps anchor count 72 -> 49, matching what `run_all.sh` ANCHOR AUDIT now
reports (the design.md main.rs line-anchors were converted to symbol refs).
- AGENTS version baseline 3b36fe3 -> 88f0923 (gate-holder merged this batch;
88f0923 is the gate-green commit of the round). Charter Code Baseline
`fc686ab` left as-is — line-number anchor semantics were not bulk-reverified
this round, so claiming otherwise would overstate (§7.4).
GATE-DOCS 6/6 clean (49 line-refs, 0 errors); GATE-P0 stood at 105/0/4.
User feedback: any finite TTL causes push to silently stop working
eventually. Stalwart does not re-verify (RFC 8620 §7.2 sends the
verification request once at subscription creation). The key is
explicitly deleted by forget_push_subscription when the subscription
is retired, so no TTL is needed.
Changes:
- PUSH_SUBSCRIPTION_SECONDS: WEEK → 0 (never expires)
- remember_push_subscription: ttl_seconds=0 → SET without EX (Redis)
/ expiry=i64::MAX (MemoryState)
- ttl_contract_is_pinned test: assert 0
- docs: reference/opengaps/design updated to 'never expires'
Root cause of push notifications failing while cron works:
- Stalwart sends the verification request (RFC 8620 §7.2) once at
subscription creation; event pushes (§7.3) arrive without
verificationCode and rely on dedup_exists("push:subscription:{id}")
to confirm prior verification.
- TTL was 300s (5 min): key expired → every event push 401'd.
- Cron path bypasses this key entirely (polls /changes directly),
which is why cron-delivered emails still arrived.
Fix: align with PUSH_REGISTRATION_SECONDS (1 week). Stalwart does not
re-verify; the key must survive restarts and any gap between
subscription creation and events.
Also fixes: removed fictional push:subscription-code:{code} from
opengaps TTL table (no such key exists in code); corrected
reference.md caller from register_push→jmap_push verification path;
fixed 前→EX typo in reference.zh-CN.md; design.md updated with
fifth 'why' bullet and historical context.
New mailbox onboarding no longer floods Telegram with the entire
mailbox history. The initial baseline walk (ReconcileCursor::Walk,
any position) now passes an EmailQueryFilter to list_emails_page that
bounds Email/query to messages received within the last 7 days and
not yet seen ($seen keyword, RFC 8621 §4.1.2).
- EmailQueryFilter { received_after: Option<i64>, unread_only: bool }
in src/domain/jmap.rs (backend-agnostic domain type).
- StalwartJmap::fetch_email_page builds a compound Filter::and from
the filter conditions (after + not_keyword "$seen").
- walk_baseline computes the 7-day cutoff once and passes it to
every page fetch, so resumed walks stay filtered.
- Replay(state) path returns early and never calls list_emails_page,
so /changes incremental is unaffected.
- const INITIAL_BASELINE_WINDOW_DAYS: u64 = 7.
- New test first_reconcile_filters_the_initial_baseline_to_recent_unread
pins the filter on every Walk page and the cursor transition to Replay.
- 3 doc anchors re-anchored (worker.rs:227→241, client.rs:485→513 ×2).
GATE-P0: 104 passed / 0 failed / 4 ignored.
The retire-on-change commit inserted ~500 lines into notify.rs and ~110
into state.rs, shifting every anchor at or after the insertion point.
The ANCHOR audit only proves the referenced line exists and is
non-empty, so all of these passed while pointing at the wrong code:
deployment.md/.zh-CN register_telegram_webhook 1619 -> 1623
register_push 1464 -> 1468 (EN)
register_telegram_webhook 1439 -> 1623 (zh)
register_push 1315 -> 1468 (zh)
retired.md/.zh-CN registration_lock fmt 1328-1330 -> 1481-1483
register_push 1315 -> 1468
state.rs read_batch 491 -> 506
state.rs query_async 531 -> 546
state.rs XGROUP CREATE 499-507 -> 514-522
opengaps.md/.zh-CN get_reconcile_state 777 -> 792
set_reconcile_state 785 -> 800
acquire_lock 1076 -> 1130
Every target was verified against the source before editing: two of the
pre-existing anchors pointed inside a different function than the one
they named, and the EN deployment.md had both register anchors off while
the zh twin still carried the original pre-move numbers.
Also documents the new push:registration:current key in the Redis key
table (writer, readers, TTL, and why its literal suffix cannot collide
with the hex session_digest suffix) and carries the zh twin the EN
version already got. Mirror the retire-on-change contract in
deployment.zh-CN.md - the zh twin has its own callback section, which
the EN update skipped.