diff --git a/CHANGELOG.md b/CHANGELOG.md index d28c8e16c..08e2838f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ ## 1.1.1 — Unreleased ### Changes -- Placeholder for upcoming patch fixes. +- Web relay now supports configurable command heartbeats (`inbound.reply.heartbeatMinutes`, default 30m) that ping Claude with a `HEARTBEAT_OK` sentinel; outbound messages are skipped when the token is returned, and normal/verbose logs record each heartbeat tick. +- New `warelay heartbeat` CLI triggers a one-off heartbeat (web provider, auto-detects logged-in session; optional `--to` override). Relay gains `--heartbeat-now` to fire an immediate heartbeat on startup. +- Added `warelay relay:tmux:heartbeat` helper to start relay in tmux and emit a startup heartbeat automatically. ## 1.1.0 — 2025-11-26 diff --git a/README.md b/README.md index 25d5d432d..f1e0684fc 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ Install from npm (global): `npm install -g warelay` (Node 22+). Then choose **on | `warelay send` | Send a WhatsApp message (Twilio or Web) | `--to ` `--message ` `--wait ` `--poll ` `--provider twilio\|web` `--json` `--dry-run` `--verbose` | | `warelay relay` | Auto-reply loop (poll Twilio or listen on Web) | `--provider ` `--interval ` `--lookback ` `--verbose` | | `warelay status` | Show recent sent/received messages | `--limit ` `--lookback ` `--json` `--verbose` | +| `warelay heartbeat` | Trigger one heartbeat poll (web) | `--provider ` `--to ` `--verbose` | +| `warelay relay:tmux:heartbeat` | Start relay in tmux and fire a heartbeat on start (web) | _no flags_ | | `warelay webhook` | Run inbound webhook (`ingress=tailscale` updates Twilio; `none` is local-only) | `--ingress tailscale\|none` `--port ` `--path ` `--reply ` `--verbose` `--yes` `--dry-run` | | `warelay login` | Link personal WhatsApp Web via QR | `--verbose` | @@ -111,12 +113,18 @@ Best practice: use a dedicated WhatsApp account (separate SIM/eSIM or business a bodyPrefix: "You are a concise WhatsApp assistant.\n\n", command: ["claude", "--dangerously-skip-permissions", "{{BodyStripped}}"], claudeOutputFormat: "text", - session: { scope: "per-sender", resetTriggers: ["/new"], idleMinutes: 60 } + session: { scope: "per-sender", resetTriggers: ["/new"], idleMinutes: 60 }, + heartbeatMinutes: 30 // optional; pings Claude every 30m and only sends if it omits HEARTBEAT_OK } } } ``` +#### Heartbeat pings (command mode) +- When `heartbeatMinutes` is set (default 30 for `mode: "command"`), the relay periodically runs your command/Claude session with a heartbeat prompt. +- If Claude replies exactly `HEARTBEAT_OK`, the message is suppressed; otherwise the reply (or media) is forwarded. Suppressions are still logged so you know the heartbeat ran. +- Trigger one manually with `warelay heartbeat` (web provider only). Use `--heartbeat-now` to fire once at relay start. + ### Logging (optional) - File logs are written to `/tmp/warelay/warelay.log` by default. Levels: `silent | fatal | error | warn | info | debug | trace` (CLI `--verbose` forces `debug`). Web-provider inbound/outbound entries include message bodies and auto-reply text for easier auditing. - Override in `~/.warelay/warelay.json`: diff --git a/docs/heartbeat.md b/docs/heartbeat.md new file mode 100644 index 000000000..d77349598 --- /dev/null +++ b/docs/heartbeat.md @@ -0,0 +1,37 @@ +# Heartbeat polling plan (2025-11-26) + +Goal: add a simple heartbeat poll for command-based auto-replies (Claude-driven) that only notifies users when something matters, using the `HEARTBEAT_OK` sentinel. + +## Prompt contract +- Extend the Claude system/identity text to explain: “If this is a heartbeat poll and nothing needs attention, reply exactly `HEARTBEAT_OK` and nothing else. For any alert, do **not** include `HEARTBEAT_OK`; just return the alert text.” +- Keep existing WhatsApp length guidance; forbid burying the sentinel inside alerts. + +## Config & defaults +- New config key: `inbound.reply.heartbeatMinutes` (number of minutes; `0` or undefined disables). +- Default: 30 minutes when a command-mode reply is configured. + +## Poller behavior +- When relay runs with command-mode auto-reply, start a timer with the resolved heartbeat interval. +- Each tick invokes the configured command with a short heartbeat body (e.g., “(heartbeat) summarize any important changes since last turn”) while reusing the active session args so Claude context stays warm. +- Abort timer on SIGINT/abort of the relay. + +## Sentinel handling +- Trim output. If the trimmed text equals `HEARTBEAT_OK` (case-sensitive) -> skip outbound message. +- Otherwise, send the text/media as normal, stripping the sentinel if it somehow appears. +- Treat empty output as `HEARTBEAT_OK` to avoid spurious pings. + +## Logging requirements +- Normal mode: single info line per tick, e.g., `heartbeat: ok (skipped)` or `heartbeat: alert sent (32ms)`. +- `--verbose`: log start/end, command argv, duration, and whether it was skipped/sent/error; include session ID and connection/run IDs via `getChildLogger` for correlation. +- On command failure: warn-level one-liner in normal mode; verbose log includes stdout/stderr snippets. + +## Failure/backoff +- If a heartbeat command errors, log it and retry on the next scheduled tick (no exponential backoff unless command repeatedly fails; keep it simple for now). + +## Tests to add +- Unit: sentinel detection (`HEARTBEAT_OK`, empty output, mixed text), skip vs send decision, default interval resolver (30m, override, disable). +- Unit/integration: verbose logger emits start/end lines; normal logger emits a single line. + +## Documentation +- Add a short README snippet under configuration showing `heartbeatMinutes` and the sentinel rule. +- Expose a CLI trigger: `warelay heartbeat` (web provider, defaults to first `allowFrom`; optional `--to` override). Relay supports `--heartbeat-now` to fire once at startup.