Repository navigation
feat(gateway): computers follow the clock through a watch, and clock writes wait for them (FD-5471) - #614
Open
earakely-scale wants to merge 8 commits into
Open
feat(gateway): computers follow the clock through a watch, and clock writes wait for them (FD-5471)#614earakely-scale wants to merge 8 commits into
earakely-scale wants to merge 8 commits into
Conversation
…writes wait for them (FD-5471) The gateway's clock gets one opaque version, a per-boot id and a count, that every set_time, advance and clear changes. A computer follows it by long-polling /clock/watch with the version it shows, which is its acknowledgement; the answer carries the clock and the gateway's received and sent stamps. The gateway provider declares the computers that follow it in CLOCK_FOLLOWER_TOKENS, each by its read token (its control token for now), and a write given wait_followers_s waits for each and answers 504 naming those behind, the change standing. set_time without a virtual_time changes an armed clock's rate from its time at the write, so a rate change no longer steps it back by a stale read. A restarted gateway is unarmed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t the URL map's (FD-5471) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… late watch never moves its computer back (FD-5471) set_time without virtual_time read the clock twice, for its t0 and for the new arm's anchor, losing the moment between them at the old rate. An acknowledgement now only moves a computer's shown version forward, a count too long to be one the clock had names nothing rather than failing the watch, and the boot id's size is a named constant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…owers (FD-5471) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…needs a rate (FD-5471) set_time given only virtual_seconds_per_real_second now changes an armed clock's rate the way advance moves it: from its time at the write, keeping t0 and generation, so time triggers keep their marks and their waits, which the change wakes to run at the new rate. set_time with neither a virtual_time nor a rate is refused with the 400 it got before, instead of re-arming at rate 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…own method; the watch contract in the card (FD-5471) - Every clock change goes through Gateway._clock_changed (triggers re-evaluated when time moved, held watches answered, the followers waited for), and the four write routes share _clock_write's body handling, which also gives set-time the JSON-object check the others had. - A rate change is the clock extension's set_rate (PUT /clock/set-rate), so set-time takes a virtual_time as it always has. - The route resolves a watch's follower from its token (ClockFollowers.follower_for), so the watch itself only acknowledges and holds. - The card advertises the watch's request and its hold (hold_s, CLOCK_WATCH_HOLD_S) and bounds wait_followers_s (CLOCK_MAX_WAIT_FOLLOWERS_S). - The clock's own tests move to clock_version_test.py, and the route tests wait for a follower to be seen rather than sleeping, with bounds a loaded runner can't miss. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1-hb11-gateway-clock-one-version-a # Conflicts: # AGENTS.md # src/agent_env/env/envs/_multi_computers.py
… (FD-5471) math.isfinite raises on an int too large for a float, and the limit messages formatted the value as one; only a float is checked for finiteness, and the messages name the limit alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
Part of FD-5420: a computer's own clock follows the gateway's universe clock. This is FD-5471, the gateway side. MCP servers read the gateway's
get_timeon every call, but a computer can't, because every program on it reads its own clock. So the computer follows the gateway through a long poll, and a clock change doesn't return until every computer shows it.What changes
set_time,advanceandclearchanges it, so a version never repeats across gateway restarts./clock/stateand every write's answer carry it.GET /clock/watch?after=<version>&follower=<name>. It answers at once when the clock's version isn'tafter, else at the next change or after 25 s. The answer is the clock's state with the gateway's wall-clockreceivedandsentstamps, which a follower uses for an NTP-style offset. A watch is its computer's acknowledgement of the version it names. It counts only when it carries that computer's read token, and never for a version the clock hasn't had.clock/v1sync_timeinCLOCK_FOLLOWER_TOKENS(name=read token), on the VM and container paths alike. For now the read token is the computer's control token, a slot a separate token can fill later with no protocol change.TOKENin the name keeps the tokens out of logged compose files.set_time,advanceandcleartakewait_followers_s(0 to 30). Given one, the write waits for every declared computer to show its version. It answers 504 naming the computers still behind, and the change itself stands. A write without one, or to a gateway with no declared computers, answers as before.set_rate(PUT /clock/set-rate). It changes an armed clock's rate from its time as of the write. Likeadvance, it keepst0and the clock's generation, so time triggers keep their relative marks and their waits, and it wakes the trigger engine, so a wait runs at the new rate.set_timestill requires avirtual_time, as before.set_timewith a time computed from a state read a moment earlier stepped the universe back by the gap. A computer, which never steps back, then ran 65–85 ms ahead.Gateway._clock_changed: it re-evaluates the triggers when time moved, answers every held watch, and waits for the followers. Future writers, such as restoring the clock after a restart (FD-5433) or a snapshot reload, can't skip a step._clock_write's body handling.watchmethod carries its request (after,follower) andhold_s(25), which a follower's request timeout must exceed. Everywait_followers_sis bounded 0–30 in the schema.Nothing on
maindeclares a follower yet: computers start servingsync_timein FD-5476. Until thenCLOCK_FOLLOWER_TOKENSis never rendered, and every clock route answers as before.Not in this PR:
wait_followers_s.Docs
The clock extension's page should cover
version, thewatchmethod,wait_followers_swith its 504, andset_timewithoutvirtual_timeas a rate change.Testing
Unit:
tst/unit/env/gateway/clock_watch_test.pyandclock_version_test.pycover:set_ratecontinuous with the clock before it, keepingt0, the generation and a relative trigger's mark;set_timewithout a time andset_ratewithout a rate refused;Provider and multi-env tests cover rendering and redacting
CLOCK_FOLLOWER_TOKENS. The full unit suite passes locally (14,387 tests), apart from one Windows disk-recipe test that fails only on macOS.E2E (
071322a2, an EC2 host in our AWS account):CLOCK_FOLLOWER_TOKENS. Each computer is a real follower process long-polling/clock/watchwith its token.wait_followers_s: 2answered 200, each acknowledged by both computers, in order. The write took 1.46 ms at p50 and 1.87 ms at p95; the prototype measured an acknowledgement wait of about 1.3 ms at p50.SIGSTOP): the write answered 504stale: ["laptop"]after 2,002 ms, and the change stood. AfterSIGCONT, the next write was acknowledged in 2.3 ms.docker restart): both followers' next watches answered unarmed on a new boot id within 46 ms of the gateway coming up, and re-arming with a wait answered 200.clear: with a wait, it answered 200 once both computers showed it unarmed.set_timewithoutwait_followers_s,clearwith no body).Remote providers (
261462b6, the CI's Modal keys):modal(a container on the private network) andmodal_vm(a container in the VM). It declared two computers, each followed through the gateway's public HTTPS tunnel:desktopfrom a Modal sandbox of its own,laptopfrom outside Modal.modaland 149 ms onmodal_vm, about two round trips through the tunnel.set_ratekeptt0, and both computers picked up ×60.clearwith a wait answered 200.Integration on the same host (local sandboxes):
gateway_test.py's clock tests pass:test_gateway_clock_extension,test_gateway_clock_rate_accuracy,test_gateway_time_trigger_extension,test_gateway_autonomous_time_triggerandtest_sync_env_clock_step.test_advance_to_next_event_hook_local,test_next_env_event_hook_local,test_register_env_triggers_localandtest_prompt_agent_notifications_local.What the e2e caught: the entrypoint parsed
CLOCK_FOLLOWER_TOKENSwith the URL-map parser, so a gateway with declared computers didn't start (fixed in91428dc1, with a unit test). It also caughtgateway_test.py's exact unarmed-state assertion (updated in071322a2).🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge; no blocking issue remains.Summary
Adds versioned clock watches, token-checked computer acknowledgements, writes that wait for followers, and continuous rate changes through
set_rate.Diagram
sequenceDiagram participant Caller participant Gateway participant Computer Computer->>Gateway: Watch with shown version and read token Caller->>Gateway: Clock write with wait_followers_s Gateway->>Gateway: Change version and wake held watches Gateway-->>Computer: New clock state Computer->>Computer: Apply clock state Computer->>Gateway: Watch naming new version Gateway-->>Caller: 200 after acknowledgements, or 504 naming stale computersReviews (12) · Last reviewed commit: "fix(gateway): a wait or rate past a floa..." · Reviewed by Greptile