Chat-loop: tell the model how long since its last reply#32
Merged
Conversation
The per-turn <datetime> tag already gives the model a clock so it
can answer "what year is it?" without guessing. It didn't tell the
model whether the user is mid-thought ("you just answered") or
reviving a thread from days ago - the model has no way to read that
from the timestamps alone, and the user shouldn't have to spell it
out every time.
Extend the tag with an optional since_last_response="..." attribute
carrying a coarse, conversational elapsed string ("about 22 hours",
"yesterday", "about 3 days") computed against the most recent
persisted assistant message's created_at. Chat.svelte walks its
messages array for the latest role==='assistant' row that isn't
marked for regenerate-from-here, and passes the timestamp through
as a new ChatLoopOptions.lastAssistantTimestamp. The chat-loop
recomputes the elapsed bucket every round (matching the existing
datetime-tag recomputation) so a long multi-tool turn doesn't ship
a stale value.
Buckets are intentionally fuzzy: the model uses this to calibrate
register, not to do arithmetic, and "yesterday" / "about 3 days"
obviously read as a stale-thread revival while "a few minutes" /
"just now" read as a live continuation. Synthetic ephemeral
injections (intuition / context-recall <think> blocks) are not
persisted and therefore not eligible anchors - the semantic is
"how long since you last actually replied to the user?". The
attribute is omitted on the opening turn (no prior assistant to
anchor against) and when the supplied timestamp doesn't parse, so
a fresh thread never carries a misleading "just now".
The system prompt's datetime block gains a paragraph teaching the
model how to read the attribute and explicitly cautioning against
quoting it back at the user verbatim or thanking them for the gap.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
SYNOPSIS
Extend the per-turn
<datetime>tag with asince_last_response="..."attribute so the model can tell a live continuation from a thread the user revived hours or days later.PURPOSE
The chat-loop already injects
<datetime local="..." utc="..." zone="..." />outside the<user_message>fence, so the model can answer "what year is it?" without guessing. It still has no signal for whether the user is mid-thought ("you just answered") or reviving an old thread ("about 3 days have passed"), and the user shouldn't have to spell that out every turn. Without it the model picks the wrong register on revival turns - charging straight back into the conversation when a brief reorientation would land better.DESCRIPTION
Existing:
buildDatetimeTag(tz)inchat-loop.tsbuilds the<datetime>tag once per round, formatted in the user'sjournalTimezonewithlocal/utc/zoneattributes.tagLastUserMessageprepends the tag outside the<user_message>fence on the latest user turn. The system prompt'sDATETIME_BLOCKteaches the model that the tag is authoritative for clock questions.Changed:
formatRelativeDuration(elapsedMs)- coarse, bucketed elapsed-time formatter:just now(<2min) /a few minutes/about N minutes/about an hour/about N hours/yesterday/about N days/about N weeks/about N months/over a year. Negative / NaN inputs collapse to "just now" so server-vs-browser clock skew doesn't surface as "in 4 seconds".buildDatetimeTag(tz, lastAssistantTimestamp?)picks up an optional anchor; on a parseable timestamp it appendssince_last_response="..."to the existing tag. Omitted otherwise, so opening turns and unparseable timestamps ship the unchanged 3-attribute tag.ChatLoopOptions.lastAssistantTimestamp?plumbs the anchor in.Chat.sveltewalks itsmessagesarray from the end for the latestrole==='assistant'row that isn't inpendingDeleteSet(regenerate-from-here rows are about to be replaced and shouldn't count as "your last reply") and passes itscreated_at. Recomputed at call time so an auto-retry after a 429 picks up assistant rows persisted on earlier rounds.DATETIME_BLOCKinchat-prompt.tsgains a paragraph teaching the model to read the new attribute as a register-calibration signal, with explicit guardrails: do NOT quote the elapsed string back at the user verbatim, do NOT thank them for the gap.How it closes PURPOSE: mid-thread turns now carry e.g.
<datetime ... since_last_response="about 22 hours" />, so the model can decide between picking up mid-thought and re-orienting briefly without the user having to flag the gap. Synthetic ephemeral injections (intuition / context-recall<think>blocks) aren't persisted and therefore not eligible anchors - the semantic is "how long since you last actually replied to the user?", not "since any assistant-role row appeared on the wire."Notes:
buildDatetimeTagrecompute), not once at send-time, so a 30s+ multi-tool turn doesn't ship a stale value.<datetime ... />tag continue to pass unchanged.docs/dev/chat.mdaccordingly; nodocs/user/change needed.Test plan
mise run checkgreen (1069 tests, 0 svelte-check errors, 0 lint warnings)describe('formatRelativeDuration', ...)block covers every bucket boundary including the singular "about an hour" branch, negative-ms clock skew, and NaNGenerated by Claude Code