Skip to content

Add hey watch - #202

Merged
monorkin merged 4 commits into
mainfrom
postings-watch
Aug 20, 2026
Merged

Add hey watch#202
monorkin merged 4 commits into
mainfrom
postings-watch

Conversation

@monorkin

@monorkin monorkin commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Adds a new hey watch command that triggers whenever an email comes in or changes.

By default hey watch will print events and their simplified, human-readable, content to STDOUT:

→ hey watch
2026-08-20T09:32:10.082Z  updated  Imbox                    GitHub — chatgpt-codex-connector[bot] left a comment (ba... (thread 2104408114)
2026-08-20T09:32:10.084Z  updated  Imbox                    GitHub —  (thread 243329991)
2026-08-20T09:32:16.264Z  updated  Imbox                    GitHub — @jeremy commented on this pull request. In scri... (thread 2104408114)

When piped the output returns JSON, which is easier to work with in a structured format:

→ hey watch | jq
{
  "change": "updated",
  "at": "2026-08-20T09:30:30.674Z",
  "box": {
    "id": XXXXXX,
    "kind": "imbox",
    "name": "Imbox"
  },
  "posting_id": XXXXXXXXXXX,
  "thread_id": XXXXXXXXXX,
  "posting": {
    "account_id": XXXXXX,
    "active_at": "2026-08-20T08:17:06Z",
    "addressed_contacts": [
      {
        "account_id": XXXXXX,
        "avatar_background_color": "#F8E71C",
        "avatar_url": "https://app.hey.com/avatars/XXXXXXXX",
...

To make scripting easier there's also the --run-async and --run-sync options which take a shell command.
Whenever something comes in the command will be run and the payload will get passed in via ENV.
The sync version runs commands one after another, waiting for one to finish before starting the next.
Async just executes commands as they come in.

@monorkin
monorkin requested a review from robzolkos August 20, 2026 09:37
@monorkin
monorkin requested a review from a team as a code owner August 20, 2026 09:37
Copilot AI balanced review requested due to automatic review settings August 20, 2026 09:37

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@robzolkos

Copy link
Copy Markdown
Collaborator

This is a thoughtful feature, and I like the core design: Action Cable provides the notification while the incremental feed remains the source of truth.

I found a few reliability gaps worth considering before merging:

1. Ensure every posting change sends a notification

Haystack sends Postings::ChangesChannel notifications from app/models/posting/broadcasting.rb, but some valid posting changes suppress that callback or bypass callbacks with update_all, including:

  • app/models/box.rb
  • app/models/user/boxes.rb
  • app/jobs/box/move_postings_by_contact_to_designated_box_job.rb
  • app/jobs/bundle/unbundle_postings_job.rb

I reproduced this on the dev server: a suppressed mark-all-seen operation changed the posting, but hey watch --events updated received nothing.

Consider separating changes-channel notifications from suppressible UI broadcasts and explicitly notifying after bulk updates. Otherwise these changes can remain invisible until another notification occurs.

2. Retry failed changes-feed reads

At internal/cmd/watch.go:358-364, a failed AllChanges request prints a warning and consumes the notification without scheduling another attempt. If the server recovers but no further email arrives, the change is never reported.

Consider scheduling another catch-up attempt with backoff while preserving the current cursor.

3. Report permanent connection failures

At internal/cmd/watch.go:310-312, closing the subscription’s Messages() channel returns success. Authorization failures and other permanent disconnects can therefore make hey watch silently exit with status 0.

Consider distinguishing expected cancellation from an unexpected subscription closure and returning an appropriate error.

4. Bound asynchronous command execution

At internal/cmd/watch.go:441-456, --run-async starts a process for every event without a concurrency limit. A --since catch-up can contain up to 2,000 changes per box, potentially launching thousands of processes.

Consider adding bounded concurrency or a queue so slow commands cannot exhaust system resources.

The overall direction is strong; addressing these cases would make the command safer and more reliable to leave running unattended.

The command was only discoverable through `hey commands`, which made it the
one top-level email command missing from `hey --help`.

Being curated means the copy has to speak HEY's language, so the description
talks about email threads rather than postings, and `watch` joins the
terminology guard that enforces it.
Retry a failed read of the changes feed on a doubling backoff instead of
consuming the notification that prompted it, report a subscription that
closes without the watch being interrupted rather than exiting 0, and cap
how many --run-async commands run at once.
@monorkin
monorkin merged commit b061bd2 into main Aug 20, 2026
27 checks passed
@monorkin
monorkin deleted the postings-watch branch August 20, 2026 17:56
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