ba00530caf
clock.stamp() (injected into her chat prompt via _now_note, and into reflection) rendered UTC and ignored the configured timezone, so 'what time is it' answered in UTC — hours off from his actual time, reading as 'she doesn't know the time'. Now converts to config.timezone (America/New_York -> EDT/EST), UTC fallback if the zone can't load. Storage stays UTC; this only changes what she reads. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
70 lines
2.4 KiB
Python
70 lines
2.4 KiB
Python
"""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
|
|
from zoneinfo import ZoneInfo
|
|
|
|
from lyra import config
|
|
|
|
|
|
def now() -> datetime:
|
|
return datetime.now(timezone.utc)
|
|
|
|
|
|
def _local_tz() -> ZoneInfo | timezone:
|
|
"""Brian's configured local zone (falls back to UTC if it can't be loaded)."""
|
|
try:
|
|
return ZoneInfo(config.load().timezone)
|
|
except Exception:
|
|
return 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 in Brian's local timezone, e.g.
|
|
'Friday, 27 Jun 2026, 01:50 EDT'. Times are stored UTC; this is what she *reads*,
|
|
so 'what time is it' answers in his time, not UTC."""
|
|
return (dt or now()).astimezone(_local_tz()).strftime("%A, %d %b %Y, %H:%M %Z")
|
|
|
|
|
|
def gap_seconds(since_iso: str | None, ref: datetime | None = None) -> float | None:
|
|
"""Seconds elapsed since `since_iso` (None -> None). The numeric counterpart to
|
|
humanize_gap, for code that needs to threshold on elapsed time."""
|
|
if not since_iso:
|
|
return None
|
|
ref = ref or now()
|
|
return max(0.0, (ref - _parse(since_iso)).total_seconds())
|
|
|
|
|
|
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"
|