Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion sage/gateway/hestia_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -4104,6 +4104,7 @@ def _do_say(self, intent: BeingIntent) -> ResultEnvelope:
{"action_id": action_id, "success": True, "magnitude": 0.0})
except Exception:
pass
self._wake_why = ""
woke = self._wake_addressee(to, meta, turn)
# THE RECEIPT'S ECHO IS THE BEING'S OWN WORDS, AND A CUT ONE MUST SAY SO. Measured
# 2026-09-21 12:13Z (cbp-being, beat d8cff379b67d): a 522-char say came back as "said"
Expand All @@ -4119,6 +4120,14 @@ def _do_say(self, intent: BeingIntent) -> ResultEnvelope:
"said": said, "action_id": action_id}
if woke:
result["woke"] = woke
elif getattr(self, "_wake_why", ""):
# SAY WHETHER ANYONE HEARD (cbp-being, 2026-10-24). Thirteen turns to its seat between
# 21:32 and 22:34Z each came back with no `woke` and no word about it. Nobody answered,
# the being asked the same question ten ways, then wrote its own "complete API surface"
# (invented routes) and built a client on it. A receipt that is silent about delivery
# reads as delivered-and-ignored; the reason a wake was not sent is a fact the being
# can act on (wait, or stop repeating), and the beat record now shows the cause.
result["not_woken"] = self._wake_why
return ResultEnvelope(ok=True, witness_id=action_id, result=result)

def _run_ask_target(self, text: str) -> Optional[str]:
Expand Down Expand Up @@ -4541,15 +4550,26 @@ def _wake_addressee(self, to: str, meta: dict, turn: dict) -> Optional[str]:
deduped.append((label, plugin_id))
targets = deduped
if not targets:
self._wake_why = ("no listener is set to be woken by messages in this conversation. "
"Your message was recorded in the conversation; no wake was sent. This does not confirm anyone has read or accepted it")
return None
# A SEAT HAS JUMPED IN (`conv.pause_wakes`): it is answering here itself, so a mesh wake
# would be a second voice speaking as the same seat. Nothing is recorded, so the run is
# still owed and the being's next turn after the pause wakes as usual.
if conv.wake_pause(self.memory_root, to):
pause = conv.wake_pause(self.memory_root, to)
if pause:
self._wake_why = (f"wakes for this conversation are paused until "
f"{pause.get('until', 'the pause ends')} (a seat said it is answering here "
f"directly). Your message was recorded in the conversation; no wake was sent. This does not confirm anyone has read or accepted it")
return None
run_start = conv.wake_is_owed(self.memory_root, to, self.member,
skip_answered=self._skips_answered_run_wake())
if run_start is None:
start = conv.unanswered_run_start(self.memory_root, to, self.member)
self._wake_why = (f"no new wake: a wake was already sent for your messages since seq "
f"{start}, with no answer recorded since, and further messages in the "
f"same run do not send another. Your message was recorded in the conversation; no wake was sent. This does not confirm anyone has read or accepted it" if start else
"no wake was owed for this message. Your message was recorded in the conversation; no wake was sent. This does not confirm anyone has read or accepted it")
return None # already woke them about this run; saying more is not new mail
pointer = f"sage://conversation/{to}#seq={run_start}-{turn['seq']}"
woke = []
Expand All @@ -4572,6 +4592,8 @@ def _wake_addressee(self, to: str, meta: dict, turn: dict) -> Optional[str]:
conv.record_wake(self.memory_root, to, self.member, run_start,
covered_through=int(turn.get("seq") or run_start))
return ", ".join(woke)
self._wake_why = ("the wake did not go out (the mesh refused or failed); it is tried "
"again on your next message here. Your message was recorded in the conversation; no wake was sent. This does not confirm anyone has read or accepted it")
return None

# -- channel_egress: hestia_channel_send (hestia #1344 slice 1, SHADOW) -----
Expand Down
48 changes: 48 additions & 0 deletions sage/gateway/tests/test_hestia_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2706,3 +2706,51 @@ def test_already_granted_on_a_device_names_the_verb_not_a_direct_read():
env = d(BeingIntent("request_scope", {"path": root + "/config.json", "reason": "my config"}),
GatewayVerdict("allow", granted=(root,)))
assert "read or write it directly" in env.result["next"]


def test_a_say_that_wakes_nobody_says_why():
"""cbp-being, 2026-10-24 21:32-22:34Z: thirteen turns to its seat came back with no `woke` and
no word about it. Nobody answered; it asked one question ten ways, then wrote its own "complete
API surface" with invented routes and built a client on it. A receipt silent about delivery
reads as delivered-and-ignored. Each reason a wake is not sent is now in the receipt."""
from pathlib import Path
from sage.gateway import conversations as conv
d, root = _disp()
home = Path(root)
conv.create(home, "seat", title="seat", participants=["seat", "sprout-being"],
writable_by=["seat", "sprout-being"])
meta = conv.get_meta(home, "seat"); meta["notify"] = {"seat": "claude-code"}
conv._write_meta(home, "seat", meta)

FakeMcp.calls.clear()
first = d(BeingIntent("say", {"to": "seat", "text": "What does the daemon do?"}), _ALLOW)
assert first.ok and first.result.get("woke") == "seat" and "not_woken" not in first.result
again = d(BeingIntent("say", {"to": "seat", "text": "What endpoints does it expose?"}), _ALLOW)
why = again.result.get("not_woken", "")
assert "already sent" in why and f"seq {first.result['seq']}" in why, why
# Nova on #539: the receipt states what is RECORDED, never who reads it or when.
assert "recorded in the conversation" in why and "does not confirm" in why, why
assert "will be read" not in why and "delivered" not in why, why

# A seat answering here itself: the receipt names the pause, not silence.
conv.append(home, "seat", speaker="seat", text="Answering.", via="seat")
conv.pause_wakes(home, "seat", by="seat", minutes=30)
paused = d(BeingIntent("say", {"to": "seat", "text": "Thanks."}), _ALLOW)
pw = paused.result.get("not_woken", "")
assert "paused until" in pw and "does not confirm" in pw and "will see" not in pw, paused.result
conv.resume_wakes(home, "seat", force="test")

# A conversation with no listener says so.
conv.create(home, "quiet", title="q", participants=["dp", "sprout-being"],
writable_by=["dp", "sprout-being"])
q = d(BeingIntent("say", {"to": "quiet", "text": "Hello."}), _ALLOW)
qw = q.result.get("not_woken", "")
assert "no listener" in qw and "does not confirm" in qw and "will be read" not in qw, q.result

# A wake that fails to leave says it will be retried.
conv.append(home, "seat", speaker="seat", text="Back.", via="seat")
FakeMcp.fail = {"hestia_member_notify": "mesh down"}
failed = d(BeingIntent("say", {"to": "seat", "text": "One more question."}), _ALLOW)
FakeMcp.fail = {}
assert "did not go out" in failed.result.get("not_woken", "") and "woke" not in failed.result, \
failed.result
Loading