Skip to content

Assistant: "N days ago" is invisible to the timeframe scanner, so the turn defaults to today #310

Description

@pliablepixels

Observed live with granite3.3:8b on the question "what was the busiest hour 2 days ago?".

The scanner misses the whole <n> <unit> ago family

scanTimeExpressions only matches rolling spans with a leading past|last|previous (app/src/lib/assistant/timeframe-stage.ts:124). Nothing matches <n> <unit> ago:

scan("what was the busiest hour 2 days ago?") = []
scan("2 days ago")   = []
scan("two days ago") = []
scan("3 hours ago")  = []
scan("last 2 days")  = ["last 2 days"]     <- only the modifier form matches

An empty scan falls through to the default at timeframe-stage.ts:219, phrases: [today], so the system prompt told the model:

Timeframes for this question, already resolved (copy these exact phrases into when): "today".

for a question naming a different day. A model that follows that instruction answers about today using real data, which reads as correct — the exact failure whenNotFromQuestion exists to prevent, arriving through a door the scanner left open.

The when guard rejects the spelled-out form

whenWordTokens splits on [^\p{L}]+, so digits are dropped and number words are not:

guard("two days ago", "what was the busiest hour 2 days ago?") -> rejected
guard("2 days ago",   "what was the busiest hour 2 days ago?") -> accepted

Identical phrases, different spelling of the number.

How they compound

The app resolved "today", the model correctly ignored that and called list_events with when: "two days ago", the app rejected it over the digit/word difference, and the model gave up and asked the user to restate the date. The turn produced nothing.

Test gap

time-eval-cases.ts carries 3 days ago as an INTERPRET case, so phrase-to-fields handles the family fine. There is no EXTRACT case for it, so nothing checked whether the scanner finds it in a question. The two stages disagree and only one is covered.

Fix

  • A <n> <unit> ago pattern in scanTimeExpressions, digits and spelled-out numbers.
  • Fold number words to digits in the guard tokens so two and 2 compare equal.
  • Extract cases for the family, so the scanner stays covered.

Ships today with qwen3:8b, which likely survives it by copying the question words verbatim.

Metadata

Metadata

Assignees

No one assigned

    Labels

    coreChanges core behavior; full review ceremony

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions