Files
project-lyra/lyra/clock.py
T
serversdown 5176c706b6 feat: thought loop — Lyra's threaded, surfaceable train of thought
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>
2026-06-21 07:05:15 +00:00

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"