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:
2026-06-22 01:39:11 +00:00
parent fef45b3e05
commit 43697f8340
4 changed files with 81 additions and 29 deletions
+39 -17
View File
@@ -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: