From 3375aae7f46769722c7aaa71fa0c1c6069f01ef1 Mon Sep 17 00:00:00 2001 From: Mickael Farina Date: Thu, 16 Jul 2026 09:34:50 +0200 Subject: [PATCH] fix(observer_recall): don't bluff on clock-time asks it can't answer (beat 20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "recap what I was doing today between 11am and 1pm" parsed as no-window (_window_seconds only handles relative spans like "20 minutes"), fell through to "summarise the whole buffer", and returned the last few minutes as if it had answered — a confident, off-topic reply. The observer is a RAM ring of the last N snapshots (~10-30 min depending on cadence), not a day log — it structurally cannot answer clock-time/calendar windows. Now it detects those asks (11am, 14:30, today, this morning, last night, …) and says so plainly, reports the span it actually covers, and still shows what it does hold. Relative asks ("20 minutes ago", "just now") are unchanged. Manifest regenerated; 52 observer tests pass, ruff clean. Co-Authored-By: Claude Opus 4.8 --- skills/.manifest.json | 2 +- skills/observer_recall.py | 45 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) 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