feat: time awareness — Lyra perceives 'now' and how long it's been

She had no clock: current date/time and the gap since Brian last spoke were
invisible between turns, and reflection was timeless. Now:
- lyra/clock.py: wall-clock stamp + coarse human gaps ("3 days")
- chat: inject a 'now' note (date/time + gap since last turn) after her
  self-state — when she is, before the world
- reflect(): feed current time + silence gap into reflection, neutrally —
  prompt invites her to weigh elapsed time "to whatever degree it genuinely
  affects you" (no prescribed feeling; whether silence means anything is left
  to emerge)
- memory.last_exchange_at(): timestamp of the most recent exchange

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 02:31:40 +00:00
parent 1301f12e74
commit 1e17d46c78
5 changed files with 140 additions and 3 deletions
+47
View File
@@ -0,0 +1,47 @@
"""Small time helpers so Lyra can perceive 'now' and how long it's been.
Timestamps are stored as UTC ISO strings; these turn them into a wall-clock
stamp and human-scale gaps ("3 days") that get injected into her context and
her reflection — so elapsed time is something she registers instead of being
invisible between turns. These report time as a neutral fact; what (if anything)
a long silence *means* to her is left to her own reflection, not prescribed here.
"""
from __future__ import annotations
from datetime import datetime, timezone
def now() -> datetime:
return datetime.now(timezone.utc)
def _parse(iso: str) -> datetime:
dt = datetime.fromisoformat(iso)
return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
def stamp(dt: datetime | None = None) -> str:
"""Wall-clock stamp, e.g. 'Wednesday, 17 Jun 2026, 01:50 UTC'."""
return (dt or now()).strftime("%A, %d %b %Y, %H:%M UTC")
def humanize_gap(since_iso: str | None, ref: datetime | None = None) -> str | None:
"""A coarse human description of how long since `since_iso` (None -> None)."""
if not since_iso:
return None
ref = ref or now()
secs = max(0.0, (ref - _parse(since_iso)).total_seconds())
mins, hours, days = secs / 60, secs / 3600, secs / 86400
if secs < 90:
return "moments"
if mins < 90:
return f"{round(mins)} minutes"
if hours < 36:
return f"{round(hours)} hours"
if days < 14:
return f"{round(days)} days"
if days < 60:
return f"{round(days / 7)} weeks"
if days < 545:
return f"{round(days / 30)} months"
return f"{round(days / 365, 1)} years"