Repository navigation
Conversation
Run a timelock event worker alongside the hourly monitor without changing alert delivery. It observes TimelockEvent over an Envio graphql-transport-ws live query plus bounded, paginated HTTP reconciliation, and records event provenance, per-chain checkpoints, reorg changes, and timing in a local SQLite inbox. It sends no alerts. - utils/graphql_ws.py: minimal graphql-transport-ws client - protocols/timelock/subscriber/: inbox, Envio queries, reconcile, worker, report - protocols/timelock/event_subscriber.py: run / reconcile-once / report / health - systemd unit (Type=notify + watchdog), install.sh, runbook, README - tests: disconnect, restart, backlog, reorg, late indexing, per-chain isolation Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Live testing against the updated Envio endpoint showed Hasura sends a keepalive ping before connection_ack, which the client rejected. Answer pings while waiting for the ack, and make the fake server do the same. TimelockEvent exposes blockHash: select it, store it per event, and let a changed hash at the same id surface as a changed row. Make two timing- sensitive worker tests deterministic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tage report - Inbox: take the write lock (BEGIN IMMEDIATE) before reading in observe, mark_missing, and set_checkpoint. Concurrent WS and HTTP observations of the same unseen id raised IntegrityError and could end the reconcile task. - Reconcile loop: an unexpected exception is logged and recorded as a failed cycle instead of ending the loop. - Backoff: cap the exponent; 2 ** n overflowed after ~1,025 failures and stopped the WebSocket task permanently. - Watchdog: count the reconcile deadline from worker start until the first cycle completes, and withhold pings when any loop task has ended. - Report: recover the connection state at the window start so an outage that began earlier is reported, clipped to the window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With TIMELOCK_SUBSCRIBER_NOTIFY=errors (run --notify errors) the pilot posts plain-text diagnostics to the errors chat, labelled [timelock-subscriber] and stored under yearn-internal: - each new event at first sighting (transport, delay, full address links); bursts collapse to one summary per chain; first runs post nothing - WS misses (HTTP found events first while the WebSocket was connected) - changed/removed/restored rows - WebSocket down >5m, chain head stalled, 3 failed reconciles, each with its recovery - a daily report summary whose schedule survives restarts Notices go through a bounded background queue that coalesces messages and drops failed sends, so Telegram never delays ingestion. They stay off when no errors chat is configured, since send_error_message would otherwise fall back to a protocol group. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Inbox schema is versioned (PRAGMA user_version). Databases from the first pilot revision gain timelock_sub_events.block_hash in place, under the write lock, keeping observations, changes, and checkpoints. Without it, ingestion failed with "no such column: block_hash". - A row stored before the event selection added blockHash fingerprints differently when re-observed. If removing the newly selected fields reproduces the stored fingerprint, upgrade the row silently instead of recording every existing event as a reorg change. - Notices use a persistent baseline: the time notices were first enabled on the inbox. Events from earlier blocks are never announced, so a fresh pilot's first WebSocket snapshot no longer posts week-old events, while events indexed during a later downtime still are. Compared at whole seconds, matching block timestamps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #438 once the live pilot is complete (see Not yet done). Part of epic #437.
What this is
A continuously running worker that observes indexed
TimelockEventrows alongside the hourlytimelock_alerts.py. It does not change alert delivery. The pilot sends no alerts. Its output goes to journald,$CACHE_DIR/timelock_subscriber.db, and optionally the internal errors chat (see below).Transports (
--transport both|ws|http, defaultboth)graphql-transport-wslive query (utils/graphql_ws.py) over the newest 50 monitored events, filtered by monitored chains and addresses. It reconnects with capped exponential backoff (1s–60s).chain_metadata.latest_processed_blockand fetches(checkpoint − overlap, head]in chunks of at mostMAX_BLOCK_SPANblocks. Pages are ordered by(blockNumber, logIndex, id).State
TIMELOCK_LAST_TS.changed(same id, new content),removed(no longer returned by a fully fetched range),restored. The old payload is kept.Edge cases
blockHash, so a reorg shows as a changed row or a removed id.Supervision
monitoring-timelock-subscriber:Type=notify,WatchdogSec=120,Restart=always.event_subscriber.py healthexits 1 on a stale heartbeat, a failed reconcile, a WebSocket outage, or a stalled chain.Report
event_subscriber.py report --hours Nshows:both,http, orundecidedTests
32 new tests drive the real worker, client, and SQLite inbox against a fake Envio (an in-process HTTP stand-in and a local graphql-transport-ws server). They cover:
Each test asserts every expected event is stored exactly once. The suite passed 8 consecutive runs.
ruff,ty, and the full suite (1957 passed) are clean.Docs and ops
protocols/timelock/README.md: pilot design.deploy/runbook.md: operations, controlled restart/backlog/disconnect tests, settings, and a failure-mode row.deploy/install.shinstalls the unit, and.env.examplelists the new variables.monitoring.yamlis unchanged because alerting behavior does not change.Live validation (2026-10-22, updated Envio endpoint)
Schema:
TimelockEventexposesblockHash. It is now selected, stored per event, and included in the fingerprint, so a replaced block shows as a changed row.Bug found and fixed: Envio's Hasura sends a keepalive
pingbeforeconnection_ack. The client now answers pings during the handshake, and the fake server in the tests does the same.HTTP backfill (30 days): 230 events across 5 chains. The result matches an independent timestamp scan exactly, with no missing and no extra events. A rerun yields no new rows and no duplicates.
WebSocket: connected on the first attempt and received the 50-row snapshot with no disconnects. A 5-chain reconcile takes about 1.5s.
Restart on the same database: still 230 rows, 0 revisions, 0 changes, and every checkpoint advanced. The reconnect snapshot was absorbed as a duplicate, and SIGTERM stops the worker cleanly.
Real new event: Aave
ProposalQueued1_26249234_585was queued at 16:04:35 UTC while the worker was stopped. On restart it was recovered by both the WebSocket snapshot and HTTP, stored once with 0 revisions.Review fixes
Concurrent ingestion:
observe,mark_missing, andset_checkpointnow take the SQLite write lock (BEGIN IMMEDIATE) before reading. The old code raisedIntegrityErrorin 40 of 40 concurrent-observation rounds; the fix brings that to 0. The reconcile loop also survives unexpected exceptions.Backoff overflow: the exponent is capped, so
2 ** ncan no longer overflow after about 1,025 failures.Watchdog: the reconcile deadline counts from worker start until the first cycle completes, and pings are withheld if any loop task has ended.
Report: the connection state at the window start comes from the last earlier log entry, so an outage that began before the window is reported, clipped to it.
Schema migration: the inbox schema is versioned. A database from the first revision gains
block_hashin place, keeping all observations and checkpoints. Without this, ingestion failed withno such column: block_hash. Rows stored beforeblockHashwas selected are upgraded silently on their next observation, not recorded as reorg changes. Tested on a copy of the live pilot database: 231 events and 5 checkpoints kept, 0 spurious changes.Startup notice baseline: notices only announce events from blocks after the time notices were first enabled on the inbox, a value that persists across restarts. A fresh pilot's first WebSocket snapshot no longer posts week-old events, while events indexed during a later downtime are still announced.
Errors-chat notices (opt-in)
TIMELOCK_SUBSCRIBER_NOTIFY=errorsmakes the pilot post plain-text diagnostics to the internal errors chat, labelled[timelock-subscriber]and stored underyearn-internal. They never go to protocol groups: notices stay off unlessTELEGRAM_TOPIC_ID_ERRORSorTELEGRAM_CHAT_ID_ERRORSis set.Notices go through a bounded background queue that combines messages queued during a send and drops failed sends, so Telegram never delays ingestion. A notice was rendered from live data (Aave
ProposalQueued, captured locally, not sent).Not yet done (blocks closing #438)
report --hours 168with the production transport decision. Envio fully supports hosted WebSockets only on Dedicated plans.🤖 Generated with Claude Code