5176c706b6
Built from her own 6-19 idea: a continuing train of thought she keeps across days, organized into threads she returns to, that she can bring TO Brian and that his feedback advances or closes. Where the dream cycle's reflect() gives isolated, overwriting reflections, the thought loop adds continuity (threads), surfacing (#6 — she leads with a thought when Brian returns after a gap), and a feedback loop (his reply folds in next pass). - lyra/thoughts.py: thought_threads + thoughts tables; think() with new/continue/respond modes; salience-gated maybe_surface(); record_response() feedback; lazy-schema _c() mirroring poker. - dream.py: curiosity stage advances the loop after reflecting (error-isolated). - chat.py: build_messages surfaces the top thread after a >=90min gap, once. - web: /thoughts feed (page + data + respond + status routes), thoughts.html, nav 💭 entry. lyra-think entry point. Every thought also lands in her journal. - clock.gap_seconds(); tests/test_thoughts.py (8 tests). Full suite 58 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
57 lines
2.0 KiB
Python
57 lines
2.0 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
|
|
|
|
|
|
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 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"
|