Skip to content

hey watch --notify: the engine under the Omarchy bar plugin, in place of hey omarchy - #243

Merged
jeremy merged 15 commits into
mainfrom
watch-notify
Aug 22, 2026
Merged

hey watch --notify: the engine under the Omarchy bar plugin, in place of hey omarchy#243
jeremy merged 15 commits into
mainfrom
watch-notify

Conversation

@jeremy

@jeremy jeremy commented Aug 21, 2026

Copy link
Copy Markdown
Member

Supersedes #236.

What

basecamp/omarchy-hey-plugin (Jason, Rob) shells out to hey for the Imbox; #236 gave it hey omarchy poll, a ten-minute poll that also diffed a fingerprint file to toast new mail. Stanko asked why not hey watch; the sharper objection is that an Omarchy-named, watch-like subcommand sullied the CLI surface — and a poll is ten minutes stale by construction (the TUI→IPC nudge in #236 covered only the TUI's own mutations). So, with generic commands only:

  • hey box imbox --json is the read, hey watch is the wake-up. The plugin runs one hey watch per shell (it becomes a service-kind plugin), re-reads the Imbox per batch of events, and hey watch already catches up from its cursor on reconnect and skips ahead on a 409 — live and self-healing, no IPC nudge: the cable reports the TUI's own mutations back within a second. The watch covers every box because a move out of the Imbox is written in the target box's feed, never the Imbox's.
  • hey watch says what state it is in. Three new lines beside the thread changes: ready once every box is caught up and the subscription is live (again after each reconnect's catch-up; a read that failed is retried and ready waits for it) — a reader that reads its own state on that line has no startup gap to reason about; disconnected when the cable drops; resync when a box changed more than the feed can list and the watch skipped ahead (this used to reach stderr only). A resync is a change (scripts run, --exit-on-first counts it); ready and disconnected are stdout-only and carry no box.
  • New mail is a watch event. Every added/updated line carries "new": true|false, and --events new selects the true ones — a union with added, updated, deleted (new alone = new mail only; added,new = arrivals plus new activity on known threads). --run-* scripts see HEY_NEW=1; hey watch --box imbox --events new --exit-on-first is "block until new mail". New = an unseen, unmuted posting whose active_at is later than the watch last recorded for it, or later than the watch's start — on HEY's clock (serverNow) — for a thread without a record; active_at moves on new mail only, so seen flips, mutes and moves are never new, a box's first read carries its backlog quietly, mail that arrived during startup is new, and a box that skipped ahead on a 409 gets its own cutoff at the cursor it skipped to. The tracker records every posting the watch reads, in every box and whatever --events reports. No state file. (An earlier cut of this PR sent the toast itself as hey watch --notify; Stanko's review — why not --run-async? — showed that flag bundled HEY semantics with a desktop's presentation. The semantics stayed here; the toast moved to the plugin, and nothing desktop-shaped is left in watch*.go.)
  • The hey omarchy group is gone (never released: v0.1.1 predates all of it and bar-status was hidden): bar-status, Omarchy: hey omarchy poll, the engine under the 37signals.hey bar plugin #236's poll, the poll state and flock, the config-degraded signalling only the poll reported, the account-scope and local-config exemptions. hey setup omarchy keeps Omarchy: hey omarchy poll, the engine under the 37signals.hey bar plugin #236's plugin-entry configuration and legacy hey-unread removal; turning toasts on is now just setting the key.

Main refreshed the cable credentials per dial itself meanwhile (WithHeaderFunc, 45b0263); this branch is rebased on it and the transport wrapper it carried is gone.

Docs: docs/omarchy.md ("The bar plugin", "New mail is a watch event", "The plugin toasts", a Generic commands; the face presents decision, follow-ups), README, the skill.

Companion: basecamp/omarchy-hey-plugin#3 (service kind, watch-first with the read on ready, debounced re-read, backoff restart, auth exit waits for the probe, and the toast: Imbox new lines debounced into one omarchy-notification-send --app-name HEY … -p [-r <id>]).

Release note

Unchanged from #236: do not release 0.2.0 until the plugin repo is public and its PR is merge-approved.

Heads-up, not this PR's: make check-surface-compat fails on main today because .surface-breaking lists hey screener list --count, which v0.1.1 never had (the script calls that entry stale). Dropping that line fixes it.

Verified

make build && make test && make lint; .surface removes nothing released (--notify/--notify-box existed only on this branch). Live: hey watch --box imbox --events new against production connects, reads HEY's clock, prints ready, and exits clean on --timeout; an earlier cut reported a hey seen from another process within ~1 s, and the reconnect/token-refresh matrix passed on it. Plugin: ./tests/run (23 node + 41 QML) and omarchy plugin validate green.

Not yet done live on the final cut: a plugin toast for mail from an external mailbox, a second one replacing it, hey seennew:false, and SUPER+CTRL+comma silencing it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Replaces the unreleased Omarchy poller with generic, event-driven notifications and improves long-running cable authentication.

Changes:

  • Adds hey watch --notify with tests and documentation.
  • Refreshes cable credentials on every reconnect.
  • Migrates Omarchy setup to configure the external bar plugin and removes legacy polling code.

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.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
.surface Adds the watch --notify surface.
README.md Documents notifications and the Omarchy plugin.
docs/omarchy.md Describes the watch-driven plugin architecture.
skills/hey/SKILL.md Adds agent guidance for --notify.
internal/cable/cable.go Refreshes credentials per cable dial.
internal/cable/cable_test.go Tests refreshed reconnect headers.
internal/cmd/accounts_test.go Removes obsolete Omarchy command coverage.
internal/cmd/local_config_trust.go Removes the deleted command’s exemption.
internal/cmd/omarchy.go Configures and migrates the bar plugin.
internal/cmd/omarchy_notify.go Removes poll-based notification state.
internal/cmd/omarchy_notify_test.go Removes obsolete poll-notification tests.
internal/cmd/omarchy_test.go Tests plugin configuration and migration.
internal/cmd/root.go Removes the hey omarchy command group.
internal/cmd/watch.go Integrates desktop notification batching.
internal/cmd/watch_notify.go Implements desktop notification behavior.
internal/cmd/watch_notify_test.go Tests notification filtering and delivery.
tests/smoke/helpers_test.go Isolates smoke-test state storage.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/cmd/watch.go Outdated
Comment thread internal/cmd/watch.go Outdated
Comment thread README.md Outdated
Comment thread docs/omarchy.md

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a7699345b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cmd/watch_notify.go Outdated
Comment thread internal/cmd/root.go
Comment thread internal/cmd/watch.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d5b291e322

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cmd/watch.go Outdated
Comment thread internal/cmd/watch_notify.go Outdated
Comment thread internal/cmd/omarchy.go Outdated

@monorkin monorkin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great fixes!
Can we replace the --notify with a --run-async? It looks like it just shells out and the run commands are exactly for that.
Then we could make the box filter generic.

@jeremy

jeremy commented Aug 21, 2026

Copy link
Copy Markdown
Member Author

🤖 @monorkin — you were right, and zooming out made it clearer why. --notify bundled two things:

Inside --notify What it really is Where it belongs
"new" = unseen, unmuted, active_at later than the watch last recorded for the thread, or than the watch's start on HEY's clock; remembered across every box so a move-in isn't new HEY semantics — state across events and the server clock the CLI, once, Go-tested
notify-send -a HEY -u low -p -r <id>, the Omarchy glyph/exec hints, replace-not-stack, Sender — Subject / N new in Imbox, one toast per read, Imbox-only via --notify-box presentation — a desktop's business, Omarchy-shaped at that the consumer: the plugin, or a user's --run-async

My defence was about the first row; your question was about the second. So 881aa9e splits them rather than sugaring one over the other:

  • new is a watch event. Every added/updated line carries "new": true|false; --events new selects the true ones and is a union with added, updated, deleted (--events new alone = new mail only; added,new = arrivals plus new activity on known threads). --run-* scripts see HEY_NEW=1. hey watch --box imbox --events new --exit-on-first is "block until new mail" — the agent primitive. The rule, the server clock and the record-every-box memory are unchanged and still tested (watch_new.go / watch_new_test.go).
  • The box filter is generic again: --box is the only one. --notify, --notify-box, notify-send and every Omarchy hint are gone from the CLI (neither flag was released, so no .surface-breaking entry).
  • The plugin toasts from the lines it already reads (Run hey watch: one live connection per shell, the Imbox read on every change omarchy-hey-plugin#3): Imbox new lines, a short debounce so one read's burst is one toast, omarchy-notification-send --app-name HEY -u low --exec … -p [-r <id>] with the replace id kept ten minutes. Flipping notify no longer restarts the watch; it only gates the toast path.
  • Any other desktop is the honest one-liner: hey watch --box imbox --events new --run-async 'notify-send -a HEY "New mail in HEY"'.

Docs (docs/omarchy.md, README, skill) and hey setup omarchy --notify's help follow; the decision record now says "Generic commands; the face presents" and means it.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 30dfcab431

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cmd/watch.go Outdated
Comment thread internal/cmd/watch.go Outdated
Comment thread internal/cmd/watch.go Outdated
jeremy added a commit that referenced this pull request Aug 21, 2026
Stanko asked on #243 whether --notify could just be --run-async. It couldn't,
and the reason it couldn't is the tell: the flag bundled two things. What
counts as new mail — unseen, not muted, active_at later than the watch's
record of the thread or than its start on HEY's clock, remembered across every
box so a move-in isn't new — is HEY's semantics and state across events, and
belongs in the CLI, decided once and Go-tested. notify-send, the app-name,
-p/-r, the Omarchy glyph and exec hints, `Sender — Subject`, one toast per
read and the Imbox-only scope are presentation, a desktop's business, and
belonged in the plugin that already owns rendering and settings.

So the CLI grows a generic, scriptable notion of new mail and loses the
desktop: every added and updated line carries "new": true|false, --events new
selects the true ones (a union with added, updated and deleted), a --run-*
script sees HEY_NEW=1, and `hey watch --box imbox --events new
--exit-on-first` is "block until new mail". The tracker always runs — one
cheap serverNow GET at startup, one map — so the line shape is stable, and a
read is classified before it is recorded, as before. --notify, --notify-box,
desktopNotifier and notifiedBoxes are gone; neither flag was released, so
.surface-breaking needs no entry. The Omarchy plugin toasts from the new
lines itself (basecamp/omarchy-hey-plugin#3).

Docs follow: docs/omarchy.md's "hey watch --notify" becomes "New mail is a
watch event" and "The plugin toasts", the decision record tightens to
"Generic commands; the face presents", README and the skill lead with
--events new, and `hey setup omarchy --notify` says it turns on the plugin's
toasts rather than running hey watch --notify.
jeremy added a commit that referenced this pull request Aug 21, 2026
…new-mail cutoff

Two Codex threads on #243. A catch-up read that failed transiently left the
box queued for a retry and returned nil, so ready was announced with a box
still behind it — a reader then took the gap for a clean picture. catchUp now
announces ready only once every box is read (readyOnceCaughtUp); a retry that
reads the last box owed announces it (retryUnread), and a drop in between
cancels the debt, since the reconnect's catch-up announces its own.

A 409 skipped a box ahead without the new-mail record learning anything about
the gap, so a thread that arrived in it and was later moved or labelled while
still unseen measured its gap activity against the watch's start and read as
new. skippedTo moves that box's cutoff up to the cursor it skipped to — HEY's
last_posting_activity_at for the box, which bounds every thread in it — and
the cutoff is the box's alone, so another box's mail is unaffected. A gap
thread moved to another box may still read as new once; the resync line is
the reader's cue to re-read either way.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a5183a5ede

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cmd/watch.go Outdated
Comment thread internal/cmd/watch.go
jeremy added a commit that referenced this pull request Aug 22, 2026
Stanko asked on #243 whether --notify could just be --run-async. It couldn't,
and the reason it couldn't is the tell: the flag bundled two things. What
counts as new mail — unseen, not muted, active_at later than the watch's
record of the thread or than its start on HEY's clock, remembered across every
box so a move-in isn't new — is HEY's semantics and state across events, and
belongs in the CLI, decided once and Go-tested. notify-send, the app-name,
-p/-r, the Omarchy glyph and exec hints, `Sender — Subject`, one toast per
read and the Imbox-only scope are presentation, a desktop's business, and
belonged in the plugin that already owns rendering and settings.

So the CLI grows a generic, scriptable notion of new mail and loses the
desktop: every added and updated line carries "new": true|false, --events new
selects the true ones (a union with added, updated and deleted), a --run-*
script sees HEY_NEW=1, and `hey watch --box imbox --events new
--exit-on-first` is "block until new mail". The tracker always runs — one
cheap serverNow GET at startup, one map — so the line shape is stable, and a
read is classified before it is recorded, as before. --notify, --notify-box,
desktopNotifier and notifiedBoxes are gone; neither flag was released, so
.surface-breaking needs no entry. The Omarchy plugin toasts from the new
lines itself (basecamp/omarchy-hey-plugin#3).

Docs follow: docs/omarchy.md's "hey watch --notify" becomes "New mail is a
watch event" and "The plugin toasts", the decision record tightens to
"Generic commands; the face presents", README and the skill lead with
--events new, and `hey setup omarchy --notify` says it turns on the plugin's
toasts rather than running hey watch --notify.
jeremy added a commit that referenced this pull request Aug 22, 2026
…new-mail cutoff

Two Codex threads on #243. A catch-up read that failed transiently left the
box queued for a retry and returned nil, so ready was announced with a box
still behind it — a reader then took the gap for a clean picture. catchUp now
announces ready only once every box is read (readyOnceCaughtUp); a retry that
reads the last box owed announces it (retryUnread), and a drop in between
cancels the debt, since the reconnect's catch-up announces its own.

A 409 skipped a box ahead without the new-mail record learning anything about
the gap, so a thread that arrived in it and was later moved or labelled while
still unseen measured its gap activity against the watch's start and read as
new. skippedTo moves that box's cutoff up to the cursor it skipped to — HEY's
last_posting_activity_at for the box, which bounds every thread in it — and
the cutoff is the box's alone, so another box's mail is unaffected. A gap
thread moved to another box may still read as new once; the resync line is
the reader's cue to re-read either way.
jeremy added a commit that referenced this pull request Aug 22, 2026
…threads too; HEY_NEW=0

Sol's advisory on #243. The floor a 409 skip-ahead sets (newMail.skippedTo)
now applies to every thread in that box, known or not: a reply the watch
missed inside the gap and then a move while still unseen measured its gap
activity against an older record and read as new. Activity at or before the
floor — the cursor the box skipped to, its last posting activity — is never
new there; mail after it is, on a known thread or a new one.

resync joins the watchable events. It is reported by default, scripts run for
it and --exit-on-first counts it, and --events can leave it out — --events
new does, so `--events new --exit-on-first` never exits on a 409 and a
notify-send one-liner never toasts on one. The plugin asks for it by name.

Scripts see HEY_NEW=0 as well as HEY_NEW=1, on top of the inherited-variable
strip, so a test for new mail never reads a parent's value.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 83faa7dd80

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cmd/watch.go Outdated
Comment thread internal/cmd/watch.go
Comment thread internal/cmd/watch.go Outdated
jeremy added 6 commits August 21, 2026 21:48
… of hey omarchy

The 37signals.hey bar plugin needed two things from hey-cli: the Imbox, and to know
when it changed. `hey omarchy bar-status` and its planned successor `hey omarchy poll`
answered both with one ten-minute poll that also diffed a fingerprint file to toast new
mail — a toast could land ten minutes after the mail, the TUI had to nudge the plugin
over IPC after its own mutations, and an Omarchy-named command that sounded watch-like
sat on the CLI surface.

`hey box imbox --json` is the read and `hey watch` is the wake-up: it catches up from
its cursor on reconnect and skips ahead on a 409, so the bar is live and self-healing,
and the cable reports the TUI's own mutations back within a second. `hey watch --notify`
is the notifier — a generic flag on a generic command: notify-send under the app-name
HEY so notification silencing applies, at most one toast per batch of changes, replaced
rather than stacked, with Omarchy's glyph and focus-the-TUI hints when Omarchy is on
PATH. What counts as new is an unseen, unmuted posting that was added, or updated with
an active_at later than the watch last recorded (or than its start) — active_at moves on
new mail only, so seen flips, mutes and moves never toast. No state file: a box's first
read is its catch-up from the server's cursor (the box's last activity, not this moment)
and is recorded but never toasted, and what the notifier remembers lives and dies with
the watch.

The cable now authorizes every dial with the credentials of the moment. actioncable-go
reuses the header it was given on each reconnect, so a watch that outlived its access
token came back with a stale one and exited with an auth error; a Transport wrapper
refreshes the Authorization or Cookie header from the auth manager per dial, which the
TUI's live updates get too.

The `hey omarchy` group goes, never released: bar-status, the poll state and its lock,
the config-degraded signalling only the poll reported, the account-scope and local-config
exemptions, and the smoke state dir's reason. `hey setup omarchy` keeps configuring the
plugin's notify key and removing the legacy hey-unread module; turning toasts on is now
just setting the key.
…ay ready/disconnected/resync

The notifier measured a thread's activity against its start only when it had no
record, and it recorded only what --events let through and only after a box's first
read, which it dropped whole. Three holes: mail that arrived while the watch was
starting up was thrown away with the backlog; with --events updated an arrival was
never recorded, so its later seen flip read as new; and a first read that ended in a
409 left the next arrival to be dropped as backlog. Now every posting the watch reads
is recorded, in every box and whatever --events reports, and a toast needs activity
later than the record — or later than the watch's start for a thread without one,
which is what keeps a first read's backlog quiet without touching what arrived since.

--notify covered every watched box, so the plugin, which watches all of them to see a
thread leave the Imbox, would have toasted The Feed and Paper Trail too while the docs
promised the Imbox. It toasts the Imbox unless --notify-box names another watched box;
a name that isn't watched is a usage error, since that toast would never come.

The watch now says so itself when its state changes: "ready" once the cursors are set
and the subscription is live, and again after each reconnect's catch-up, so a reader
that reads its own state on that line has no gap to reason about; "disconnected" when
the cable drops; "resync" when a box changed more than the feed can list and the watch
skipped ahead, which used to reach stderr only and left a watcher stale. A resync is a
change — scripts run for it and --exit-on-first counts it; ready and disconnected go to
stdout alone, since a script runs per change and neither is one. Their box is omitted,
so the event's box is a pointer now and the environment leaves the box variables out.

Rebased onto main, which meanwhile refreshed the cable credentials per dial itself
(WithHeaderFunc); the transport wrapper here is gone in its favour.
…eave the plugin's notify alone

A drop and the reconnect that followed it reached the loop through two channels, so a
reconnect that completed while a slow catch-up held the loop could be announced ready
and then, from the other channel, disconnected — a reader following the watch's own news
would sit offline until the next reconnect. The cable's callbacks now queue every
transition in order behind one wake-up, and the loop drains the queue in that order: a
drop is announced, a reconnect catches up and is announced ready.

The cutoff between backlog and new mail was the workstation's clock, compared with the
server's active_at: a clock behind the server toasts backlog, a clock ahead sits on new
mail. The watch now reads HEY's own clock off the Date header of one cheap, uncached
request before the cursors are taken, and falls back to the local clock only when the
server's can't be read.

hey setup omarchy --remove deleted the bar plugin's notify key. The key is the plugin's:
the panel and `omarchy bar set` write it as readily as setup does, so removal cannot
tell a preference it wrote from one it didn't. --remove leaves it and says so;
--no-notify is the explicit off switch.
Stanko asked on #243 whether --notify could just be --run-async. It couldn't,
and the reason it couldn't is the tell: the flag bundled two things. What
counts as new mail — unseen, not muted, active_at later than the watch's
record of the thread or than its start on HEY's clock, remembered across every
box so a move-in isn't new — is HEY's semantics and state across events, and
belongs in the CLI, decided once and Go-tested. notify-send, the app-name,
-p/-r, the Omarchy glyph and exec hints, `Sender — Subject`, one toast per
read and the Imbox-only scope are presentation, a desktop's business, and
belonged in the plugin that already owns rendering and settings.

So the CLI grows a generic, scriptable notion of new mail and loses the
desktop: every added and updated line carries "new": true|false, --events new
selects the true ones (a union with added, updated and deleted), a --run-*
script sees HEY_NEW=1, and `hey watch --box imbox --events new
--exit-on-first` is "block until new mail". The tracker always runs — one
cheap serverNow GET at startup, one map — so the line shape is stable, and a
read is classified before it is recorded, as before. --notify, --notify-box,
desktopNotifier and notifiedBoxes are gone; neither flag was released, so
.surface-breaking needs no entry. The Omarchy plugin toasts from the new
lines itself (basecamp/omarchy-hey-plugin#3).

Docs follow: docs/omarchy.md's "hey watch --notify" becomes "New mail is a
watch event" and "The plugin toasts", the decision record tightens to
"Generic commands; the face presents", README and the skill lead with
--events new, and `hey setup omarchy --notify` says it turns on the plugin's
toasts rather than running hey watch --notify.
…new-mail cutoff

Two Codex threads on #243. A catch-up read that failed transiently left the
box queued for a retry and returned nil, so ready was announced with a box
still behind it — a reader then took the gap for a clean picture. catchUp now
announces ready only once every box is read (readyOnceCaughtUp); a retry that
reads the last box owed announces it (retryUnread), and a drop in between
cancels the debt, since the reconnect's catch-up announces its own.

A 409 skipped a box ahead without the new-mail record learning anything about
the gap, so a thread that arrived in it and was later moved or labelled while
still unseen measured its gap activity against the watch's start and read as
new. skippedTo moves that box's cutoff up to the cursor it skipped to — HEY's
last_posting_activity_at for the box, which bounds every thread in it — and
the cutoff is the box's alone, so another box's mail is unaffected. A gap
thread moved to another box may still read as new once; the resync line is
the reader's cue to re-read either way.
The box a catch-up left behind may be the one that rings next, and its read
clears it: announce the owed ready there too, rather than waiting for the
retry that may be two minutes off. TestWatchDoorbellReadPaysTheReadyACatchUpOwed.
jeremy added 3 commits August 21, 2026 21:48
…iables

Two Codex threads on a5183a5. A drop queued while a catch-up was reading —
noteConnection appended it, listen hadn't drained it — let ready go out with
the subscription already down; readyOnceCaughtUp now looks at the queue and
withholds it, so the drain says disconnected and the reconnect's catch-up
says its own ready. And scriptCommand built its environment on os.Environ,
so a watch started by another watch's script handed HEY_NEW=1 or a thread
id to events that never set them; the event's variables are stripped from
the inherited environment first.
…threads too; HEY_NEW=0

Sol's advisory on #243. The floor a 409 skip-ahead sets (newMail.skippedTo)
now applies to every thread in that box, known or not: a reply the watch
missed inside the gap and then a move while still unseen measured its gap
activity against an older record and read as new. Activity at or before the
floor — the cursor the box skipped to, its last posting activity — is never
new there; mail after it is, on a known thread or a new one.

resync joins the watchable events. It is reported by default, scripts run for
it and --exit-on-first counts it, and --events can leave it out — --events
new does, so `--events new --exit-on-first` never exits on a 409 and a
notify-send one-liner never toasts on one. The plugin asks for it by name.

Scripts see HEY_NEW=0 as well as HEY_NEW=1, on top of the inherited-variable
strip, so a test for new mail never reads a parent's value.
… no ready on the way out

Three Codex threads on 83faa7d. The server bakes a box's last posting activity
into its cursor, so mail that landed between the watch reading HEY's clock and
reading the box list sat behind the cursor and was never read: noLaterThan
moves such a cursor back to the start, and the catch-up reads it as new.
followConnection took the whole transition queue at once, so a drop queued
behind a reconnect was invisible to dropQueued while the reconnect caught up;
it now takes one transition at a time. And readyOnceCaughtUp no longer pays
a ready to a watch that --exit-on-first has already finished or an interrupt
has cut short.
Sol's finding. serverNow returned the Date header as it stood when the
server answered, so mail that landed while the request was in flight — its
updated_at no later than Date — made a box cursor no later than the start,
which noLaterThan left where it was, and the mail sat behind the initial
cursor for good. An SDK retry could widen that window by seconds. The Date is
now translated back to the request's start by the monotonic elapsed time, so
a slow or retried request only moves the start earlier; the fallback is the
local clock at the start for the same reason. Two tests: a delayed response
puts the start at least the delay before Date, and the whole window end to
end — a posting landing during the clock request, the box's cursor moved back
to the start, the catch-up reading it, new: true.
@jeremy

jeremy commented Aug 22, 2026

Copy link
Copy Markdown
Member Author

Sol's clock-window finding, fixed in eabf979: serverNow now translates the Date header back to the request's start by the monotonic elapsed time (at.Add(-time.Since(started))), so mail that lands while HEY is answering is later than the start, its box cursor is moved back by noLaterThan, and the catch-up reads it as new. A slow or SDK-retried request only moves the start earlier, and the fallback is the local clock at the start.

Tests: TestServerNowIsTheClockWhenTheRequestBeganNotWhenItWasAnswered (a delayed response puts the start at least the delay before Date) and TestWatchReadsMailThatLandedWhileItReadTheClock (end to end — slow clock request, a posting landing inside it, the box cursor rewound to the start, new: true, then ready).

Also noted: the 409 cross-box case is the documented residual, and the skip-ahead test's "moved" posting stays in its box — my recap overstated it. make lint, go test -race ./internal/cmd ./internal/cable, make test green.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eabf9791df

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cmd/watch.go
jeremy added 2 commits August 22, 2026 00:53
Codex on eabf979: started was nanosecond-precise while every cursor and
active_at is milliseconds, the feed answers what is strictly later than its
cursor, and isNew asks for strictly later than the start — so mail in the
start's own millisecond was neither read nor new. cutoffBefore truncates the
start to the millisecond and steps one back, on both the server's clock and
the local fallback.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b1e1dba664

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cmd/watch.go Outdated
Comment thread internal/cmd/watch_new.go
…as it is classified

Two Codex threads on b1e1dba. --box used to pick the boxes the watch read, so
activity in the others was never recorded and an unseen reply in The Feed
followed by a move into the Imbox read as new under the documented
`--box imbox --events new`. Every box is followed now and --box picks the
boxes whose changes are reported; a --box that names nothing is still not
found. And a read's postings were classified against the pre-read record and
recorded afterwards as a batch, so a posting carried twice by one read could
be new twice; classify records each posting as soon as it is decided.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c2bb571b6a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cmd/watch.go Outdated
Comment thread internal/cmd/watch.go
…tops with its last reported box

Two Codex threads on c2bb571. A drop could be queued between readyOnceCaughtUp
looking at the queue and announcing: the look and the announcement are now one
critical section with noteConnection's queueing, so a drop lands before the
look and withholds the ready, or after the ready is out, in the order things
happened. And since every box is followed but only the --box ones reported,
stopWatching gives up when the last reported box is gone rather than when the
last box is: a watch following the rest for the record alone would sit there
for good, reporting nothing.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8c68f04174

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/cmd/watch.go Outdated
Codex on 8c68f04: a 409 on a box the server no longer lists drops the box,
and when another reported box remains that path returned nil — so readBox
still reported a resync for a box that is gone, inviting a re-read of
nothing and ending --exit-on-first on it. skipAhead now says whether it
moved the cursor, and a resync is reported only when it did.
@jeremy
jeremy merged commit 0f189d7 into main Aug 22, 2026
23 checks passed
@jeremy
jeremy deleted the watch-notify branch August 22, 2026 08:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants