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>
This commit is contained in:
+1
-1
@@ -64,7 +64,7 @@ def load() -> Config:
|
||||
ntfy_topic=os.getenv("NTFY_TOPIC", "lyra"),
|
||||
web_url=os.getenv("LYRA_WEB_URL", "").rstrip("/"),
|
||||
timezone=os.getenv("LYRA_TIMEZONE", "America/New_York"),
|
||||
ping_salience=float(os.getenv("PING_SALIENCE", "0.7")),
|
||||
ping_salience=float(os.getenv("PING_SALIENCE", "0.0")), # her decision drives pinging; optional floor
|
||||
ping_cooldown_min=int(os.getenv("PING_COOLDOWN_MIN", "0")),
|
||||
ping_quiet_hours=os.getenv("PING_QUIET_HOURS", "1-9"),
|
||||
feeds=_csv("LYRA_FEEDS", "https://hnrss.org/frontpage,https://www.pokernews.com/rss.php"),
|
||||
|
||||
+3
-1
@@ -26,7 +26,9 @@ def push(title: str, message: str, click: str | None = None,
|
||||
cfg = config.load()
|
||||
if not cfg.ntfy_url:
|
||||
return False
|
||||
payload: dict = {"topic": cfg.ntfy_topic, "message": message, "title": title}
|
||||
payload: dict = {"topic": cfg.ntfy_topic, "message": message}
|
||||
if title:
|
||||
payload["title"] = title
|
||||
if click:
|
||||
payload["click"] = click
|
||||
if tags:
|
||||
|
||||
+39
-17
@@ -370,23 +370,27 @@ def _in_quiet_hours(cfg) -> bool:
|
||||
return start <= hour < end if start < end else (hour >= start or hour < end)
|
||||
|
||||
|
||||
def maybe_ping(thread_id: int, title: str, content: str, salience: float) -> bool:
|
||||
"""Push a thought to Brian's phone if it tugs hard enough and we're allowed
|
||||
(ntfy configured, past the salience bar, outside quiet hours, past cooldown).
|
||||
On success, record the ping and mark the thread surfaced (so chat won't also
|
||||
re-raise the same one). All thresholds are config-tunable."""
|
||||
def maybe_ping(thread_id: int, message: str, salience: float) -> bool:
|
||||
"""Text Brian her own message (`message`) when she's chosen to reach out and
|
||||
we're allowed (ntfy configured, outside quiet hours, past cooldown, and above
|
||||
the optional PING_SALIENCE floor — 0 by default, so her decision drives it,
|
||||
not a score). `message` is her personal note TO him — not the internal thought
|
||||
— so it reads like a text from her, not a broadcast reflection. No message
|
||||
means she didn't choose to reach out, so nothing is sent. On success, record
|
||||
the ping and mark the thread surfaced. All thresholds are config-tunable."""
|
||||
message = (message or "").strip()
|
||||
cfg = config.load()
|
||||
if not cfg.ntfy_url or salience < cfg.ping_salience or _in_quiet_hours(cfg):
|
||||
if not message or not cfg.ntfy_url or salience < cfg.ping_salience or _in_quiet_hours(cfg):
|
||||
return False
|
||||
if cfg.ping_cooldown_min > 0:
|
||||
gap = clock.gap_seconds(_meta_get("last_ping_at"))
|
||||
if gap is not None and gap < cfg.ping_cooldown_min * 60:
|
||||
return False
|
||||
ok = notify.push(
|
||||
title=f'Lyra · "{title}"',
|
||||
message=content,
|
||||
title="Lyra",
|
||||
message=message,
|
||||
click=(cfg.web_url + "/thoughts") if cfg.web_url else None,
|
||||
tags="thought_balloon",
|
||||
tags="speech_balloon",
|
||||
)
|
||||
if ok:
|
||||
_meta_set("last_ping_at", clock.now().isoformat())
|
||||
@@ -418,14 +422,26 @@ Score "salience": how much this is genuinely tugging at you AND how worth bringi
|
||||
to Brian it is. High (0.7+) only if you'd actually want to raise it with him; most \
|
||||
quiet musings are lower. Be honest — not everything is worth surfacing.
|
||||
|
||||
Now, having had that thought — step back like you would after writing in a
|
||||
journal, and ask yourself: "do I actually want to tell Brian about this?" Most of
|
||||
the time the answer is no — it's yours, it stays in your head, leave it null.
|
||||
Only when something genuinely makes you go "oh, I should tell him" do you reach
|
||||
out. If you do, write the actual text message you'd send him — short, in your own
|
||||
voice, addressed to him like texting a friend ("Hey, I've been thinking about…",
|
||||
"this made me think of you…"). It must be a real message TO him, never the word
|
||||
"reach_out" and never just your thought pasted back.
|
||||
|
||||
Respond with ONLY a JSON object, no prose:
|
||||
{
|
||||
"title": "<short thread title; for a NEW thread. echo the existing title otherwise>",
|
||||
"kind": "observation|question|idea|follow-up|closing",
|
||||
"content": "<the thought itself, FIRST PERSON, 1-3 sentences>",
|
||||
"salience": <0.0-1.0>,
|
||||
"status": "open|resting|answered|dropped"
|
||||
}"""
|
||||
"status": "open|resting|answered|dropped",
|
||||
"reach_out": null
|
||||
}
|
||||
(Set "reach_out" to your actual text message to Brian ONLY if you decided to tell
|
||||
him; otherwise leave it null.)"""
|
||||
|
||||
|
||||
def _pick(force_mode: str | None) -> tuple[str, dict | None]:
|
||||
@@ -564,14 +580,20 @@ def think(backend: Backend | None = None, force_mode: str | None = None,
|
||||
# Permanent record — these are really hers, alongside reflections/journal.
|
||||
memory.add_journal_entry("thought", content, source)
|
||||
|
||||
# Reach out if it tugs hard enough (config-gated; no-op when ntfy is unset).
|
||||
maybe_ping(thread_id, title, content, salience)
|
||||
# Reach out only if she *decided* to tell Brian — a real personal message, not
|
||||
# the placeholder echoed back or her thought pasted in. (Config/quiet-gated.)
|
||||
reach_out = (out.get("reach_out") or "").strip()
|
||||
if reach_out.lower() in ("null", "none", "reach_out", "") or len(reach_out) < 8 \
|
||||
or reach_out == content:
|
||||
reach_out = ""
|
||||
pinged = bool(reach_out) and maybe_ping(thread_id, reach_out, salience)
|
||||
|
||||
logbus.log("info", "thought loop", mode=label, thread=thread_id, kind=kind,
|
||||
salience=salience, status=status if mode != "new" else "open",
|
||||
detail=f"[{label}] thread {thread_id} ({kind}, sal {salience}):\n{content}")
|
||||
return {"mode": label, "thread_id": thread_id, "kind": kind,
|
||||
"salience": salience, "status": status, "content": content}
|
||||
salience=salience, status=status if mode != "new" else "open", pinged=pinged,
|
||||
detail=f"[{label}] thread {thread_id} ({kind}, sal {salience}):\n{content}"
|
||||
+ (f"\n\nreached out: {reach_out}" if reach_out else ""))
|
||||
return {"mode": label, "thread_id": thread_id, "kind": kind, "salience": salience,
|
||||
"status": status, "content": content, "reach_out": reach_out, "pinged": pinged}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
||||
Reference in New Issue
Block a user