Files
project-lyra/lyra/notify.py
T
serversdown 43697f8340 fix: ntfy ping is her personal text to Brian, by her decision — not a thought dump
Feedback: the push broadcast her raw internal thought ("Eelis Parssinen's
victory is a reminder...") — read like a journal entry, not her texting him.

Now the flow matches the intent: she thinks/journals, then *decides* "I should
tell Brian about this." think() asks for an optional `reach_out` — a real text
message addressed TO him in her own voice, written only when she chooses to. The
ping sends that message (title "Lyra", like a text from her), never the internal
thought. No reach_out = nothing sent (most thoughts stay hers).

- Pinging decoupled from the salience score: her decision (a reach_out) drives it,
  not a threshold. PING_SALIENCE is now an optional floor (default 0.0).
- Defensive: reject the placeholder echo ("reach_out"), too-short junk, or the
  thought pasted back as the message.
- notify.push: title now optional (omitted -> cleaner text-style notification).

Verified live: 3 passes kept private; a decided reach-out lands as a personal
text. Suite 67 green, ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 01:39:11 +00:00

47 lines
1.8 KiB
Python

"""Outbound push so Lyra can reach Brian when he's not in the app (ntfy).
This is the literal version of what she asked for — thinking "unprompted, without
you" only matters if she can also *reach* you. When a thought tugs hard enough,
the thought loop calls `push()` here and it lands on your phone with a tap-through
to the Thoughts feed. One-way: you reply in the app, which feeds the loop.
Transport only. Whether/when to ping (salience bar, cooldown, quiet hours) is the
thought loop's call — see `thoughts.maybe_ping`.
"""
from __future__ import annotations
import httpx
from lyra import config, logbus
def push(title: str, message: str, click: str | None = None,
tags: str | None = None, priority: str | None = None) -> bool:
"""Publish a notification to the configured ntfy topic. Returns True on success.
Never raises — a down ntfy must not break the thought loop.
Uses ntfy's JSON publishing (POST to the base URL) rather than headers, so
UTF-8 titles/messages (em-dashes, smart quotes, her actual words) go through —
HTTP headers are latin-1 only and choke on them."""
cfg = config.load()
if not cfg.ntfy_url:
return False
payload: dict = {"topic": cfg.ntfy_topic, "message": message}
if title:
payload["title"] = title
if click:
payload["click"] = click
if tags:
payload["tags"] = [t.strip() for t in tags.split(",") if t.strip()]
if priority:
payload["priority"] = priority
try:
r = httpx.post(cfg.ntfy_url, json=payload, timeout=8.0)
ok = r.status_code < 400
if not ok:
logbus.log("error", "ntfy push failed", status=r.status_code)
return ok
except Exception as exc:
logbus.log("error", "ntfy push error", error=str(exc)[:160])
return False