You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
hey watch --exit-on-first --timeout 20s exited at once on every run, printing an old change — for example {"change":"added","at":"2026-09-18T23:45:39.083Z","box":{"kind":"imbox",...},"new":false,...} eight days after the fact. A plain hey watch reported that backlog on startup as if it had just happened, and --exit-on-first ("wait for one change of any kind") never waited. --events new was unaffected only because the stale line was new: false.
Why
The first read of each box started from the since in the box's posting_changes_url, on the understanding that HEY bakes its clock into it. It does not:
HEY builds that since from Box#last_posting_activity_at: the latest updated_at among the box's unbundled postings, or the box's own updated_at when it has none. The changes feed also answers deletions and bundled postings, so they can come later than it. On the real account an empty Reply Later has a since from 2020 and answers a deletion from 2026-08-19 on every start.
/boxes.json reaches the watch through the SDK's ETag cache. HEY's ETag for it is fresh_when etag: @boxes.to_a, meaning the box rows, and posting activity never touches a box. So a 304 serves the since as it stood when the list was first cached. On the real account the cached Imbox since was 18:13:37Z while the live one was 18:33:09Z, and the watch reported the posting in between (new: false, created before the watch began). This is the reported Imbox symptom.
noLaterThan, which moved a later since back to the watch's start, never fired against HEY. Its cursors carry microseconds (iso8601(6)), and the millisecond layout refused to parse them.
What changes
Every box's first read starts at the watch's start. That start is on HEY's clock (serverNow), and only the version is kept from HEY's URL (watchStartSince replaces noLaterThan). The catch-up reports only what happened after the start, so --exit-on-first waits. Mail that lands between reading HEY's clock and reading the box list is still read, and still counts as new.
The start can be up to a second early, and the docs say so. HEY's Date header is whole seconds, so a change from up to a second (plus the request's time) before the watch began can still be reported, and its at shows when it happened. Nothing HEY serves gives a finer server time: Action Cable pings are Time.now.to_i, no JSON answer carries a server "now", and next_incremental_sync_url is whole seconds too. Rounding the other way would skip changes that came after the start. TestWatchReadsTheWholeSecondHEYsClockWasReadIn pins this.
A skip-ahead (409 → resync) now moves to HEY's clock at the skip, keeping the feed version from the box's URL. It used to take the listed since, and after a drop long enough for 2,000 changes the ETag-cached box list could hand back the very cursor that had fallen behind, so every read answered 409 again. The resync line's at and the box's new-mail floor are the skip point: HEY's clock when it answered (serverNowAnswered), not reduced by the request's time. A resync has no gap to catch, and a point pushed back by a slow or retried request could leave a busy feed still behind. An interrupt or --timeout during that clock read ends quietly. A calendar's 409 skips the same way.
A skip reads its list past the SDK cache (newUncachedSDKClient). The list's ETag doesn't change when the feed version does, and HEY answers 409 for a version it no longer serves, so a cached list could loop. A list or clock read that fails is retried on the backoff, and an interrupt during one ends quietly. A feed still too busy after a skip is one recovery: the first skip is read from immediately, later skips wait on the doubling retry backoff instead of every doorbell, and the one resync goes out with the clean read that ends it, with at set to the last skip.
Unchanged:--since still reads back first.
Calendars get the same treatment. The recording feeds and the calendar list now start at the watch's start (calendarCursor; calendarCursorNoLaterThan is gone). The list's since is the latest calendar updated_at, so the first poll of every watch used to report a calendar deleted after the rest last changed.
A watch that cannot read HEY's clock no longer starts.serverNow used to fall back to the workstation's clock. Both the feed cursors and the new-mail cutoff are measured against that start, so a fast local clock would skip changes and call new mail old, and a slow one would report history. It now returns an error when the request fails or the answer has no Date header. A request that fails there would fail at the box list next anyway.
Separate commit: a skip-ahead's new-mail floor (newMail.skippedTo) was parsed with the same millisecond layout, so no floor was ever set against HEY. It now reads RFC 3339 with any fraction.
Help text, docs/cli.md, docs/omarchy.md, the skill and AGENTS.md now describe this behaviour.
Tests
The new tests run against a hermetic fake HEY whose changes feed answers only what is strictly later than its cursor, as HEY's does. The fake guards its state with a mutex, so the tests are race-free. On main these fail:
TestWatchDoesNotReportHistoryAsItStarts: with --exit-on-first, a stale Imbox since and an empty Reply Later with a later deletion produce only ready, and the watch keeps waiting. A reply that lands after the start is then reported, new, and ends the watch.
TestWatchReadsMailThatLandedBeforeItReadTheBoxes: mail that lands after the clock read but before the box list is reported and new, and none of the history is.
TestWatchPollDoesNotReportHistoryAsItStarts: the first calendar-list poll doesn't report a calendar deleted before the start, but does report one deleted after it.
TestWatchedBoxesStartAtTheWatchsStart and TestCalendarCursor: every cursor starts at the start and keeps HEY's version, whatever since HEY served.
TestNewMailAfterASkipAheadIsSinceTheSkip: sets a floor from a microsecond cursor.
TestServerNowRefusesWithoutHEYsClock (replaces the local-clock fallback test): an unreachable server and an answer with no Date header are both errors.
TestWatchSkipAheadGetsPastACachedBoxList: a box list served from the SDK's cache (304) after a 409 leads to a single resync and then the next change. Before the fix it produced a second resync. TestWatchSkipsAheadToHEYsClock, TestWatchReportsAResyncAfterSkippingAhead and TestWatchCalendarSkipsAheadOnAFullSync pin the skip point, the resync's at, the floor and the feed version. TestWatchSkipAheadEndsQuietlyWhenInterrupted covers an interrupt during the skip's clock read, for both a box and a calendar.
TestWatchSinceReadsTheHistoryFirst passes both before and after: --since still reports the backlog, marked not new, and then ready. The reconnect and ready tests are unchanged.
Checked read-only against a real account. The released build printed a posting created before the watch began and exited. This branch printed {"change":"ready"} and waited out the 20s timeout.
Summary by cubic
Fixes hey watch reporting stale history as news on startup, so --exit-on-first now waits for a change instead of exiting on an old one.
Each box's first read now starts at the watch's own start, read off HEY's clock, keeping only the version from HEY's posting_changes_url. HEY's since is a box's last posting activity, not its clock, and can arrive stale through the SDK's ETag cache; neither is reported as new anymore. Mail that lands between the clock read and the box list is still reported and new. HEY's clock is read to the whole second and taken back by the time the request took, so a change from up to about a second before the watch began — plus that request's time — can also be reported, be new, and end --exit-on-first; its at says when it happened. Calendars get the same treatment for the calendar list and recording feeds. --since still reads back history first.
A skip-ahead (409 → resync) now moves the cursor to HEY's clock at the skip rather than to the box's listed cursor: the box list is ETag-cached on its box rows, so after a long drop a 304 handed back the since the watch had already fallen behind from, and every subsequent read answered 409 again. The box or calendar list is still read for the feed's version and to confirm the feed exists — read past the SDK's cache, since a new feed version need not change that ETag, and HEY refuses a version it no longer speaks. The resync line's at and the new-mail floor are the skip point, read with RFC 3339 (any fraction) so HEY's microsecond cursors set a floor. The skip point is HEY's clock when it answered, not taken back by the request's time: a resync has no gap to catch, and a point taken back by a slow or retried request could leave a busy feed still too far behind.
A 409 straight after a skip is the same catch-up, one resync for the whole episode: the next skip waits on the retry backoff while the feed stays too busy, and the resync goes out with the clean read that ends the episode, at the last skip, so a reader re-reading on it missed nothing a later skip passed. A list or clock read that fails is warned about and held for that backoff — doorbells leave a held feed alone so they don't keep repeating the failing reads — and an interrupt or --timeout during one ends the skip quietly, leaving the cursor where it was. Calendars skip the same way.
The watch refuses to start when HEY's clock cannot be read (request failure or missing Date header), instead of falling back to the workstation's clock, which can be out of sync with HEY.
Written for commit f07e7ff. Summary will update on new commits.
[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
The reason will be displayed to describe this comment to others. Learn more.
Approved. Cursor Security Agent completed successfully with no findings that need human review, and Cursor Bugbot was not running on this PR. No reviewers were assigned.
Sent by Cursor Approval Agent: Pull Request Approver
This comment says a skip-ahead resumes from HEY's cursor, but skipAhead now keeps only the listed feed version and replaces Since with serverNow. That contradicts both the implementation and the skip-ahead documentation below; describe the two caller-specific replacements instead.
Later recovery skips discard changes without notifying consumers
internal/cmd/watch.go:724
A later skip in the same recovery can discard a new range of changes, but this branch suppresses the only signal telling consumers to re-read. For example, a consumer may react to the first resync immediately, then the feed can return another 409 and skip from that first cutoff to a later one; those intervening changes are never emitted, and no second resync or final recovery event tells the consumer its refreshed view is stale. Emit a signal for every successful skip, or defer the single signal until the recovery has reached a clean read.
Suppressing later resync signals leaves consumers with stale calendars
internal/cmd/watch_calendar.go:363
Suppressing calendar_resync after the first skip can leave consumers stale. If a consumer re-reads the calendar when the first signal arrives and a later retry skips another overloaded interval, recordings in that later gap are neither emitted nor followed by another re-read signal. Emit on each successful skip, or wait until a clean feed read before emitting the one recovery signal.
On Copilot's "previously missed" notes in the review of f2c271c (watch.go:724, watch_calendar.go:363, where a later skip in the same recovery could pass over changes after the one resync had already gone out): fixed in b6adbff. The recovery's single resync/calendar_resync is now sent with the clean read that ends it, after every skip, and its at is the last skip. The first skip is read from immediately, so a skip that works first time isn't left waiting for a doorbell. A 409 on that read or any later one still waits on the retry backoff. TestWatchRecoversFromARepeated409Once asserts that nothing is written while the feed keeps answering 409, that exactly one resync appears at the clean read, with the last skip as its at, and that a second recovery gets its own. On f2c271c it fails with an early resync.
Reset shared backoff when removing a disappearing box
internal/cmd/watch.go:856
If this box disappears while a skip is being retried, stopWatching removes it from unread but never calls settleBackoff. A recovery that reached the two-minute cap therefore leaves the shared backoff at two minutes, so the next unrelated transient box/calendar failure waits two minutes instead of restarting at two seconds. Settle the backoff after removing the box.
Reset shared backoff when removing a disappearing calendar
internal/cmd/watch_calendar.go:446
When a calendar disappears during a retried recovery, removing it from calendar.unread does not reset the shared backoff. After repeated 409s, a later unrelated transient failure can consequently inherit the two-minute delay instead of starting at two seconds. Settle the backoff after dropping the calendar.
On the "previously missed" backoff notes in the review of b6adbff (stopWatching / stopWatchingCalendar not calling settleBackoff): this needs a box or calendar to disappear from the account in the middle of a skip-ahead retry. Even then, the only effect is that the next unrelated transient failure waits on the elevated backoff (at most two minutes) before retrying, instead of two seconds. Nothing is lost, since the cursor and retry still work. This is rare and self-correcting, and I haven't changed it in this PR.
hey watch --exit-on-first --timeout 20s exited at once on every run,
printing a posting from eight days before; a plain watch reported that
backlog on startup as if it had just happened.
The first read of each box started from the since in its
posting_changes_url, on the understanding that HEY bakes its clock in.
It does not. HEY builds that since from Box#last_posting_activity_at —
the latest updated_at among the box's unbundled postings, or the box's
own updated_at when it has none — while the feed also answers deletions
and bundled postings, so an empty Reply Later answers a deletion from
last month on every start. And /boxes.json reaches the watch through
the SDK's ETag cache: HEY's ETag for it is the box rows, which posting
activity never touches, so a 304 serves the since as it stood when the
list was first cached. Against the real account the cached Imbox since
lagged the live one by twenty minutes, and the watch reported the
posting in between. noLaterThan, which moved a later since back to the
start, never fired against HEY either: its cursors carry microseconds,
which the millisecond layout refused to parse.
Every box now starts at the watch's start, keeping only the version from
HEY's URL. The catch-up reports what happened after the start and
nothing before, so --exit-on-first waits for a change; mail that lands
between reading HEY's clock and reading the box list is still read, and
is new. --since still reads back first, and a skip-ahead still resumes
from HEY's since. The calendars' recording feeds and the calendar list
start the same way: the list's since is the latest calendar updated_at,
so a calendar deleted after the rest last changed was reported by the
first poll of every watch.
A box that answers 409 is skipped ahead to the since in its
posting_changes_url, and that since becomes the box's floor: activity at
or before it is never new there, because the watch never read the gap.
The floor was read in the watch's millisecond layout, and HEY writes the
since to the microsecond, so the parse failed and no floor was ever set
against the real server — a thread moved or labelled while unseen in the
gap could read as new mail. The since is now read as RFC 3339 with any
fraction.
serverNow falls back to the workstation's clock when HEY's cannot be
read, and a start on that clock is only as good as the clock: a fast one
puts it in HEY's future, where every change until then would go unread.
The start now says which clock it came from. On HEY's, a feed starts at
the start as before; on the workstation's alone, HEY's own since is kept
when it is the earlier of the two, or cannot be read. That may report
the history this branch set out to suppress, but only when HEY's clock
could not be read, and missing mail is worse than repeating it.
The previous commit kept HEY's cursor when only the workstation's clock
said when a watch began, but new mail is measured against that same
start: a fast clock would still call mail that arrived after startup
old, and a slow one would call replayed history new, and the docs
promised a start that the fallback did not keep.
serverNow now returns an error when HEY's clock cannot be read — the
request failed, or the answer carried no Date header — and the watch
exits with it rather than guess. A request that fails there would fail
at the box list next anyway. Every feed starts at the start again
(watchStartSince), with no second rule to describe.
HEY's Date header is whole seconds, so the start read off it can be up to
a second — plus the request's own time — before the watch really began,
and a change from that window is read, reported, can be new, and can end
--exit-on-first. Nothing HEY serves says the time any finer: Action
Cable's pings carry Time.now.to_i, no JSON answer carries a server "now",
and the one sync URL built from the clock (a box's
next_incremental_sync_url) is whole seconds too. Rounding the other way
would skip up to a second of changes that came after the start, which is
worse than repeating one that did not.
The help text, docs/cli.md, docs/omarchy.md, the skill and AGENTS.md now
say so, and a test pins the boundary: a change in the Date header's own
second is reported, with its at, and one from before it is not.
A read that answers 409 skipped ahead to the since in the box's
posting_changes_url, read from a fresh box list. The list comes through
the SDK's ETag cache, and HEY's ETag for /boxes.json is the box rows,
which posting activity never touches: after a drop long enough for 2,000
changes, the 304 handed back the since the watch had fallen behind from,
the next read answered 409 again, and every doorbell after that was
another resync that did not advance. Even a fresh since is the box's last
posting activity, which a deletion or a bundled posting can come later
than.
A skip-ahead now moves the cursor to HEY's clock at the skip, read the
way the watch's start is, keeping the feed's version from the box's URL.
The box list is still read for that version and to learn whether the box
is gone. The resync line's at is the skip point, and so is the box's
new-mail floor. A clock that cannot be read leaves the cursor where it
was, to be tried again on the retry backoff, as any failed read is.
A calendar's 409 skips the same way. Its list is not stale the same way,
since a recording touches its calendar and so the list's ETag, but its
since is still the calendar's updated_at rather than HEY's clock, and
one rule is easier to reason about than two.
The start is the Date header taken back by the whole of the clock request, retries included, so a change can be reported from a second before the watch began plus however long that request took, not about a second. The help text, docs/cli.md, docs/omarchy.md, the skill and AGENTS.md now say so.
An interrupt or --timeout while a skip-ahead read HEY's clock fell through to the retry path: it warned that the box or calendar could not be skipped ahead and armed a retry the ending watch would never run. Both skip-aheads now check the context first, as the feed reads beside them do, and return quietly with the cursor where it was.
watchCursor's comment still said a skip-ahead resumes from HEY's since. Neither caller keeps it without --since: a first read starts at the watch's start and a skip-ahead at HEY's clock, both keeping the feed version.
A skip-ahead took its point from serverNow, the watch start's reading: the Date header taken back by the whole request, retries included. The start needs that, to catch what lands while it is asked. A skip has already given up the gap, which the resync line says, so it has nothing to catch there, and a point taken back by a slow or retried request could leave a busy feed still too far behind to follow. Skip-aheads now read serverNowAnswered: the millisecond before the Date header.
A skip-ahead keeps the feed version from the box or calendar list and
replaces only the since. The list comes through the SDK's ETag cache, and
HEY's ETag for /boxes.json is the box rows — for /calendars.json the
calendars and the selection — which a new feed version need not change.
If HEY moved a feed to a new version, the 304 handed back the old one,
HEY answers 409 for a version it no longer speaks
(Changes::BaseController#version_mismatch?), and every read after every
skip was another 409 and another resync.
The SDK has no per-request way past its cache, so newUncachedSDKClient
builds a sibling of the CLI's client from the same configuration and
options, minus the cache, scoped to the same account. Skip-aheads read
their list through it; a 409 is rare enough to build one each time.
…rrupt
The clock read in a skip-ahead already told an interrupt from a failure,
but the box and calendar list reads before it returned any error
straight out of the watch: Ctrl-C or --timeout during the read exited
with an error, and a passing 500 ended a watch that would have recovered
on the next try. All three reads — the uncached client, the list and the
clock — now go through skipFailed: an interrupt ends quietly, a
permanent failure (credentials, a malformed request) still ends the
watch, and anything else warns and retries on the backoff with the
cursor where it was.
The "too much changed ... skipping ahead" notice was printed before the
skip was tried, so a skip that failed still claimed one. It now follows
the skip, beside the resync line.
A skip-ahead lands on HEY's clock at the whole second before its Date
header. A feed busy enough to carry more than an increment's worth of
changes after that answers 409 again on the next read, and every
doorbell after it — as many as the feed was changing — read, skipped and
reported another resync with no delay between them.
A feed's recovery from a 409 is now one episode (feedRecovery). The
first skip is announced with its notice and its resync line and counts
as the feed read. A 409 straight after it skips again without a word and
leaves the box or calendar on the retry backoff, which doubles while the
feed stays that busy; doorbells for it wait for that retry rather than
skipping it again. A clean read ends the episode, and the next 409 after
that is a recovery of its own.
The help text, docs/cli.md, the skill and AGENTS.md say one resync
covers the whole catch-up.
The docs said nothing HEY serves gives a finer time than the Date header. A posting doorbell's at (Posting::Broadcasting, iso8601_with_ms) and the feeds' cursors carry microseconds, but only once something has changed, never as a now before the watch starts. AGENTS.md and serverNow now say that. docs/omarchy.md also said a box's first read carries only what arrived while the watch was starting; it now includes the whole-second window described just before it.
A skip-ahead whose list or clock read failed put the box or calendar on the retry backoff, but doorbells kept reading it: each one met the same 409 and made the same failing requests, and the backoff waited for nothing. The failed skip now marks the feed holding, as a repeated 409 does, so doorbells leave it to the retry. A skip that lands clears the hold, and the feed follows its doorbells again.
A recovery announced its resync at its first skip and stayed quiet for
the skips after it. A reader that re-read the box on that resync could
then be left stale: a later skip in the same recovery passed over
changes that were neither reported nor followed by another cue to
re-read.
The resync now goes out when the recovery ends, with the first clean
read, after every skip it took; its at is the last skip. So that a
skip that works first time is not left waiting for the next doorbell,
the first skip of a recovery is read from straight away. A 409 on that
read, or on any later one, still waits on the retry backoff. Boxes and
calendars alike.
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
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.
hey watch --exit-on-first --timeout 20sexited at once on every run, printing an old change — for example{"change":"added","at":"2026-09-18T23:45:39.083Z","box":{"kind":"imbox",...},"new":false,...}eight days after the fact. A plainhey watchreported that backlog on startup as if it had just happened, and--exit-on-first("wait for one change of any kind") never waited.--events newwas unaffected only because the stale line wasnew: false.Why
The first read of each box started from the
sincein the box'sposting_changes_url, on the understanding that HEY bakes its clock into it. It does not:sincefromBox#last_posting_activity_at: the latestupdated_atamong the box's unbundled postings, or the box's ownupdated_atwhen it has none. The changes feed also answers deletions and bundled postings, so they can come later than it. On the real account an empty Reply Later has asincefrom 2020 and answers a deletion from 2026-08-19 on every start./boxes.jsonreaches the watch through the SDK's ETag cache. HEY's ETag for it isfresh_when etag: @boxes.to_a, meaning the box rows, and posting activity never touches a box. So a 304 serves thesinceas it stood when the list was first cached. On the real account the cached Imboxsincewas18:13:37Zwhile the live one was18:33:09Z, and the watch reported the posting in between (new: false, created before the watch began). This is the reported Imbox symptom.noLaterThan, which moved a latersinceback to the watch's start, never fired against HEY. Its cursors carry microseconds (iso8601(6)), and the millisecond layout refused to parse them.What changes
serverNow), and only the version is kept from HEY's URL (watchStartSincereplacesnoLaterThan). The catch-up reports only what happened after the start, so--exit-on-firstwaits. Mail that lands between reading HEY's clock and reading the box list is still read, and still counts as new.atshows when it happened. Nothing HEY serves gives a finer server time: Action Cable pings areTime.now.to_i, no JSON answer carries a server "now", andnext_incremental_sync_urlis whole seconds too. Rounding the other way would skip changes that came after the start.TestWatchReadsTheWholeSecondHEYsClockWasReadInpins this.resync) now moves to HEY's clock at the skip, keeping the feed version from the box's URL. It used to take the listedsince, and after a drop long enough for 2,000 changes the ETag-cached box list could hand back the very cursor that had fallen behind, so every read answered 409 again. Theresyncline'satand the box's new-mail floor are the skip point: HEY's clock when it answered (serverNowAnswered), not reduced by the request's time. A resync has no gap to catch, and a point pushed back by a slow or retried request could leave a busy feed still behind. An interrupt or--timeoutduring that clock read ends quietly. A calendar's 409 skips the same way.newUncachedSDKClient). The list's ETag doesn't change when the feed version does, and HEY answers 409 for a version it no longer serves, so a cached list could loop. A list or clock read that fails is retried on the backoff, and an interrupt during one ends quietly. A feed still too busy after a skip is one recovery: the first skip is read from immediately, later skips wait on the doubling retry backoff instead of every doorbell, and the oneresyncgoes out with the clean read that ends it, withatset to the last skip.--sincestill reads back first.calendarCursor;calendarCursorNoLaterThanis gone). The list'ssinceis the latest calendarupdated_at, so the first poll of every watch used to report a calendar deleted after the rest last changed.serverNowused to fall back to the workstation's clock. Both the feed cursors and the new-mail cutoff are measured against that start, so a fast local clock would skip changes and call new mail old, and a slow one would report history. It now returns an error when the request fails or the answer has no Date header. A request that fails there would fail at the box list next anyway.newMail.skippedTo) was parsed with the same millisecond layout, so no floor was ever set against HEY. It now reads RFC 3339 with any fraction.Help text,
docs/cli.md,docs/omarchy.md, the skill and AGENTS.md now describe this behaviour.Tests
The new tests run against a hermetic fake HEY whose changes feed answers only what is strictly later than its cursor, as HEY's does. The fake guards its state with a mutex, so the tests are race-free. On
mainthese fail:TestWatchDoesNotReportHistoryAsItStarts: with--exit-on-first, a stale Imboxsinceand an empty Reply Later with a later deletion produce onlyready, and the watch keeps waiting. A reply that lands after the start is then reported, new, and ends the watch.TestWatchReadsMailThatLandedBeforeItReadTheBoxes: mail that lands after the clock read but before the box list is reported and new, and none of the history is.TestWatchPollDoesNotReportHistoryAsItStarts: the first calendar-list poll doesn't report a calendar deleted before the start, but does report one deleted after it.TestWatchedBoxesStartAtTheWatchsStartandTestCalendarCursor: every cursor starts at the start and keeps HEY's version, whateversinceHEY served.TestNewMailAfterASkipAheadIsSinceTheSkip: sets a floor from a microsecond cursor.TestServerNowRefusesWithoutHEYsClock(replaces the local-clock fallback test): an unreachable server and an answer with no Date header are both errors.TestWatchSkipAheadGetsPastACachedBoxList: a box list served from the SDK's cache (304) after a 409 leads to a singleresyncand then the next change. Before the fix it produced a secondresync.TestWatchSkipsAheadToHEYsClock,TestWatchReportsAResyncAfterSkippingAheadandTestWatchCalendarSkipsAheadOnAFullSyncpin the skip point, the resync'sat, the floor and the feed version.TestWatchSkipAheadEndsQuietlyWhenInterruptedcovers an interrupt during the skip's clock read, for both a box and a calendar.TestWatchSinceReadsTheHistoryFirstpasses both before and after:--sincestill reports the backlog, marked not new, and thenready. The reconnect and ready tests are unchanged.Checked read-only against a real account. The released build printed a posting created before the watch began and exited. This branch printed
{"change":"ready"}and waited out the 20s timeout.Summary by cubic
Fixes
hey watchreporting stale history as news on startup, so--exit-on-firstnow waits for a change instead of exiting on an old one.Each box's first read now starts at the watch's own start, read off HEY's clock, keeping only the version from HEY's
posting_changes_url. HEY'ssinceis a box's last posting activity, not its clock, and can arrive stale through the SDK's ETag cache; neither is reported as new anymore. Mail that lands between the clock read and the box list is still reported and new. HEY's clock is read to the whole second and taken back by the time the request took, so a change from up to about a second before the watch began — plus that request's time — can also be reported, be new, and end--exit-on-first; itsatsays when it happened. Calendars get the same treatment for the calendar list and recording feeds.--sincestill reads back history first.A skip-ahead (409 →
resync) now moves the cursor to HEY's clock at the skip rather than to the box's listed cursor: the box list is ETag-cached on its box rows, so after a long drop a 304 handed back thesincethe watch had already fallen behind from, and every subsequent read answered 409 again. The box or calendar list is still read for the feed's version and to confirm the feed exists — read past the SDK's cache, since a new feed version need not change that ETag, and HEY refuses a version it no longer speaks. The resync line'satand the new-mail floor are the skip point, read with RFC 3339 (any fraction) so HEY's microsecond cursors set a floor. The skip point is HEY's clock when it answered, not taken back by the request's time: a resync has no gap to catch, and a point taken back by a slow or retried request could leave a busy feed still too far behind.A 409 straight after a skip is the same catch-up, one resync for the whole episode: the next skip waits on the retry backoff while the feed stays too busy, and the resync goes out with the clean read that ends the episode, at the last skip, so a reader re-reading on it missed nothing a later skip passed. A list or clock read that fails is warned about and held for that backoff — doorbells leave a held feed alone so they don't keep repeating the failing reads — and an interrupt or
--timeoutduring one ends the skip quietly, leaving the cursor where it was. Calendars skip the same way.The watch refuses to start when HEY's clock cannot be read (request failure or missing
Dateheader), instead of falling back to the workstation's clock, which can be out of sync with HEY.Written for commit f07e7ff. Summary will update on new commits.