Skip to content

Push notifications + one-tap unblock (ntfy/Telegram)#621

Closed
bborn wants to merge 2 commits into
mainfrom
task/4467-push-notifications-one-tap-unblock-ntfyt
Closed

Push notifications + one-tap unblock (ntfy/Telegram)#621
bborn wants to merge 2 commits into
mainfrom
task/4467-push-notifications-one-tap-unblock-ntfyt

Conversation

@bborn

@bborn bborn commented Jun 23, 2026

Copy link
Copy Markdown
Owner

What

A first-class push notifier for TaskYou's weakest spot: getting notified — and able to act — when a task needs you, away from the machine. Borrows herdr's mobile story (ntfy push + one-tap action).

Pushes fire on task lifecycle events and let you resume a blocked agent with one tap from your phone.

How it works

  • internal/notify — a provider-agnostic Notifier with two providers:
    • ntfy (simplest): JSON publish with structured action buttons.
    • Telegram (optional second provider): inline deep-link buttons.
  • No parallel path. It plugs into the existing events.Emitter through a small Notifier interface. Every mutation path already routes through that emitter (executor, MCP taskyou_needs_input/taskyou_complete, CLI, TUI), so all of them produce pushes. Notifications run on the emitter's wait group, so short-lived CLI/MCP commands flush them before exit.
  • OFF by default — nothing is sent unless notify_enabled=true and a provider is configured. Settings are read live from the DB (no restart).

Events → pushes

Event Push
task.blocked (incl. taskyou_needs_input) 🔔 Needs input + one-tap reply action
task.auth_required 🔐 Auth required + one-tap reply action
task.completed ✅ Completed
task.failed ❌ Failed

Body carries task title + project + a short reason (the reason falls back to the latest taskyou_needs_input question for a meaningful message).

One-tap unblock

needs-input / auth-required pushes carry an ntfy http action that POSTs to the existing POST /api/tasks/{id}/input endpoint with a canned reply (default "continue"), which tmux send-keys into the executor pane and resumes the agent. Plus an "Open task" deep link to type a custom reply. Action links use notify_base_url (falls back to http://localhost:<http_api_port>).

Config (ty settings)

notify_enabled, notify_base_url, notify_unblock_reply, notify_ntfy_server, notify_ntfy_topic, notify_ntfy_token (secret), notify_telegram_token (secret), notify_telegram_chat_id. Token keys are hidden from ty settings and the settings API. Documented in docs/notifications.md + README.

Tests

  • Provider unit tests (ntfy payload + action assembly with auth, Telegram deep-link buttons).
  • Event allow-list filtering, default-off behavior, reason enrichment, base-URL fallback.
  • End-to-end through the real wiring: db.UpdateTaskStatus(blocked)events.Emitternotify → HTTP publish, asserting the action targets POST /api/tasks/{id}/input.

go build ./..., go test, and golangci-lint run (v2.8.0) pass on all touched packages.

Acceptance check

  • ✅ Blocking a task / taskyou_needs_input delivers a push within seconds (verified end-to-end in tests through the real emitter path).
  • ✅ The push's tap action hits the existing input API that resumes the agent (POST /api/tasks/{id}/inputtmux send-keys, the same endpoint ty-web already uses).
  • ✅ Completed/failed pushes work; config documented; tests for the notifier + event wiring.

Follow-ups / notes

  • Telegram can't POST from inline buttons, so it gets an "Open task" deep link rather than a true one-tap reply — ntfy is the recommended provider for one-tap unblock.
  • For phone access, the daemon HTTP API must be reachable (VPN/tailnet/tunnel); notify_base_url points at that reachable origin.

🤖 Generated with Claude Code

bborn and others added 2 commits June 23, 2026 09:02
Adds a first-class push notifier that fires on task lifecycle events
(blocked/needs-input, auth-required, completed, failed) and lets you act
from your phone with one tap.

- internal/notify: provider-agnostic Notifier with ntfy (JSON publish,
  structured action buttons) and Telegram (deep-link buttons) providers.
  OFF by default; settings read live from the DB.
- Wires into the existing events.Emitter via a Notifier interface — every
  mutation path (executor, MCP, CLI, TUI) already routes through it, so no
  parallel event path. Fires on the emitter's wait group so short-lived
  CLI/MCP commands flush pushes before exit.
- needs-input / auth-required pushes carry a one-tap ntfy "http" action that
  POSTs to the existing POST /api/tasks/{id}/input endpoint to resume the
  agent, plus an "Open task" deep link. Reason falls back to the latest
  taskyou_needs_input question for a meaningful body.
- Config via `ty settings` (notify_enabled, notify_ntfy_*, notify_telegram_*,
  notify_base_url, notify_unblock_reply); token keys are secret-hidden.
- Tests: provider unit tests, event-mapping/filtering, reason enrichment, and
  end-to-end DB → emitter → notifier → HTTP through the real wiring.
- Docs: docs/notifications.md + README feature bullet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…deliver

Live end-to-end testing surfaced a race: the notifier read its settings
from the DB inside the async delivery goroutine, but short-lived CLI
commands (e.g. `ty status`) `defer database.Close()` the moment Run
returns — before PersistentPostRun flushes the emitter wait group. The
goroutine then read a closed DB, saw notifications as disabled, and
silently dropped the push. (Daemon/MCP were unaffected: the DB stays open.)

- events.Notifier.Notify now returns a delivery closure. The emitter calls
  Notify synchronously (DB guaranteed open) and runs the returned closure on
  its wait group; the closure performs only network I/O, touching no DB.
- notify.Notifier.Notify reads settings/logs and builds the message up front,
  returns the sender closure (nil = nothing to send). Adds Deliver() for
  synchronous callers/tests.
- Surface delivery failures: daemon executor wires logf to its logger;
  openTaskDB attaches a stderr logf gated by TY_NOTIFY_DEBUG. Best-effort
  pushes were otherwise completely silent on failure.
- Update events/notify tests for the new signature.

Verified live: blocking a task delivered an ntfy push (title + project +
the actual needs-input question), and tapping the http action POSTed to
/api/tasks/{id}/input and landed the reply in the agent's tmux pane.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bborn bborn marked this pull request as ready for review June 26, 2026 17:33
bborn added a commit that referenced this pull request Jun 26, 2026
…very race

Ports the two wins from PR #621 onto the mobile-console notifier:

1. One-tap unblock. ntfy pushes for actionable events (blocked/auth_required)
   now carry an Actions header: an http button that POSTs the canned reply to
   the existing POST /api/tasks/{id}/input (resuming the agent without opening
   anything), plus a view button deep-linking into the mobile console (/m) for
   a custom reply — console and one-tap, together. Reply text is configurable
   via notify_reply (default "continue"); validated live against ntfy.sh.

2. CLI-exit delivery race fix. The notifier read its settings inside the async
   delivery goroutine, but short-lived CLI/MCP commands defer db.Close() the
   instant Run returns — before PersistentPostRun flushes the emitter wait
   group — so the goroutine hit a closed DB, saw notifications as disabled, and
   silently dropped the push (daemon/web runs were unaffected; their DB stays
   open). Notifier.Prepare now reads settings + builds the request synchronously
   and returns a network-only delivery closure; events.Emit calls it before
   spawning the goroutine.

Existing header/webhook behavior and tests are unchanged; adds tests for the
action button (default + custom reply, and no action when no base URL is set).
@bborn

bborn commented Jun 26, 2026

Copy link
Copy Markdown
Owner Author

Closing in favor of #614, which is the broader feature (phone-first mobile console + push notifications). The two unique wins from this PR — the one-tap ntfy action button (POST to /api/tasks/{id}/input) and the CLI-exit delivery race fix — have been ported onto #614 in commit e0a1894. Consolidating onto one notifier avoids two parallel implementations. See #614 (comment)

@bborn bborn closed this Jun 26, 2026
bborn added a commit that referenced this pull request Jun 29, 2026
Rework after factoring in PR #621 (internal/notify). Email becomes a provider in
that framework rather than a standalone sidecar: outbound fires on the same
events as ntfy/Telegram; inbound IMAP poller runs as a daemon goroutine; thread
-map lives in the main ty DB; replies resume via the existing POST
/api/tasks/{id}/input handler (unified with push/web/TUI). Adds taskyou_notify
MCP tool for agent-initiated FYI. The extensions/ty-email sidecar is retired and
its IMAP/SMTP/allowlist/loop-protection logic ported in. Depends on #621.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant