diff --git a/skills/.manifest.json b/skills/.manifest.json index 05cc8aa..9de10db 100644 --- a/skills/.manifest.json +++ b/skills/.manifest.json @@ -62,7 +62,7 @@ "network_info.py": "bd776b619cf7c18d67fe03cb0f0456cf9c4f9bf71475740a233a9ca1e6672fcd", "notes.py": "7d50d1544ea955f59917a1f0e7902d115e9dbccd3188b641057a55b6a5b2803a", "notification_reader.py": "681208ae4253dfe549512cce4c722c76ca85f2bbbdb8737e37c026ec9444c972", - "observer_recall.py": "3744907b24ffbfbb385a630791c08ecd440f6107f21894af80ceb07191f5eb1f", + "observer_recall.py": "f7084d11c069603d815e648dd765628bfed6d31c1df4ac59f0dac35259aa6fe2", "password_generator.py": "f11a917299e14cbd2560111da0bb748cd08792cf715cc4098c64eb62da8c54e3", "philips_hue.py": "fa831712c39dc6327c84199d8f0aeb09a169a264ce3d936cc337a5c5d6632f7e", "pilot.py": "f9967890f138bc7a48ae4abc8b4170dca13961b81659ef4e530dc619c1cf90d4", diff --git a/skills/observer_recall.py b/skills/observer_recall.py index 0a74ac0..45a9a52 100644 --- a/skills/observer_recall.py +++ b/skills/observer_recall.py @@ -43,6 +43,37 @@ def _load_entries() -> tuple[list, str]: return [], "" +# Absolute / clock-time asks the buffer can NEVER answer: it's a RAM ring of the +# last N snapshots (~10 min at the active cadence), not a day log. Without this +# check, "between 11am and 1pm" parsed as no-window → summarised the last few +# minutes and looked like a confident, wrong answer. +_ABSOLUTE_TIME_RE = re.compile( + r"\b\d{1,2}\s*(?:am|pm)\b" + r"|\b\d{1,2}:\d{2}\b" + r"|\b(?:today|yesterday|this morning|this afternoon|this evening|" + r"last night|earlier today|tonight)\b", + re.IGNORECASE, +) + + +def _is_absolute_time_request(task: str) -> bool: + """True for clock-time / calendar-day asks ("between 11am and 1pm", "today", + "this morning") — windows the ring buffer structurally cannot cover.""" + return bool(_ABSOLUTE_TIME_RE.search(task or "")) + + +def _coverage(entries: list) -> str: + """Human span the buffer actually covers, e.g. "06:59–07:32 (33 min)".""" + if not entries: + return "nothing" + first, last = _parse_ts(entries[0]), _parse_ts(entries[-1]) + if not first or not last: + return f"{len(entries)} snapshots" + mins = max(0, int((last - first).total_seconds() // 60)) + return (f"{first.astimezone().strftime('%H:%M')}–" + f"{last.astimezone().strftime('%H:%M')}, about {mins} min") + + def _window_seconds(task: str) -> int | None: """Parse a lookback window from the task. Returns seconds, or None for 'all'. @@ -140,6 +171,20 @@ def run(task: str, context: str = "") -> str: "clipboard, and recent files each minute." ) + # Clock-time / calendar asks ("between 11am and 1pm", "today", "this + # morning") are structurally unanswerable: the observer is a RAM ring of the + # last N snapshots, not a day log. Say so plainly — the old code fell through + # to "summarise everything" and returned the last few minutes as if it had + # answered the question. + if _is_absolute_time_request(task): + return ( + f"I can't recall that window. The observer is a rolling in-memory " + f"buffer of the last {len(entries)} snapshots — right now it only " + f"covers {_coverage(entries)} — not a full-day log, and it's wiped " + f"whenever the service restarts. Here's everything it currently " + f"holds:\n\n{_summarise(entries)}" + ) + win = _window_seconds(task) if win is not None: cutoff = datetime.now(timezone.utc).timestamp() - win