Skip to content

fix(tools): require timezone on time tools, inject user timezone into the prompt - #257

Open
WSHAPER wants to merge 4 commits into
nextcloud:mainfrom
WSHAPER:fix/timezone-handling
Open

WSHAPER wants to merge 4 commits into
nextcloud:mainfrom
WSHAPER:fix/timezone-handling

Conversation

@WSHAPER

@WSHAPER WSHAPER commented Oct 7, 2026 •

Copy link
Copy Markdown

Fixes #3
Fixes #231

timezone is now a required, pytz-validated parameter on schedule_event / add_task / update_task and the scheduled-task tools; a missing or invalid IANA name returns an actionable error to the model instead of silently resolving the time in server/UTC. The system prompt now carries the requesting user's actual timezone (OCS /cloud/user) plus the current time in that tz.

The find_details_of_current_user instruction stays as a fallback for profiles without a timezone. freebusy_finder's naive utcnow() became tz-aware UTC.

For #231, schedule_event now builds the event with icalendar instead of the ics library (which serializes any aware datetime to UTC): DTSTART/DTEND keep the supplied timezone as DTSTART;TZID=…. The organizer's mailto tolerates profiles without an email, as before.

Heads-up: tool callers (incl. MCP) that omitted timezone previously got a silently mis-timezoned event; they now get an explicit error, which should be the canonical behavior, in my opinion.

Verified on stable35 / context_agent 2.9.1, user tz America/New_York on a CEST server: "tomorrow at noon" stores as 12:00 America/New_York (DTSTART:...T160000Z).

@WSHAPER
WSHAPER requested a review from kyteinsky October 7, 2026 10:49
…server's

Time-related tools treated the timezone parameter as optional and
silently dropped it when the model omitted it, so "tomorrow at noon"
resolved against server-local or UTC time. The system prompt tried to
compensate by asking the model to discover the user's timezone through
a find_details_of_current_user tool call first — that costs an extra
agent round trip and still leaves the tools unvalidated, so any model
that skipped the discovery step produced events at the wrong
wall-clock time.

Making the timezone a required, pytz-validated parameter on every tool
that interprets a date moves the failure to the tool boundary, where a
missing or invalid IANA name returns an actionable error to the model
instead of silently scheduling six hours off. Injecting the requesting
user's actual timezone (OCS /cloud/user) into the system prompt gives
the model the correct value up front, keeping the tool-based lookup
only as a fallback for profiles without a timezone set.

Naive datetime.utcnow() in the free-slot search was replaced with
timezone-aware UTC because mixing naive and aware comparisons produced
wrong free/busy windows at DST boundaries.

Verified end to end against a CEST server with the user's timezone set
to America/New_York: the system prompt logs the current time in NY and
"tomorrow at noon" is stored as DTSTART:20261007T160000Z (12:00 EDT).

Assisted-by: opencode:zai/glm-5.3
Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com>
…g to UTC

schedule_event built VEVENTs with the ics library, which serializes any
aware datetime to UTC. The stored event kept the right instant but lost
the timezone it was created in: edits across DST boundaries and anyone
reading the raw ICS lost the anchor (issue nextcloud#231, same class as nextcloud#3 —
correct data, wrong localization contract). Building the event with
icalendar, already a dependency, keeps DTSTART/DTEND as DTSTART;TZID=…
exactly as supplied. The organizer's email is optional in the user
profile, so the mailto value must tolerate None the way the previous
Organizer constructor did.

Verified end to end: 'tomorrow at noon' from an America/New_York
profile on a CEST server stores as
DTSTART;TZID=America/New_York:…T120000.

Assisted-by: opencode:zai/glm-5.3
Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com>
@WSHAPER
WSHAPER force-pushed the fix/timezone-handling branch from 45f2db4 to 5b0730a Compare October 7, 2026 10:55
Comment thread ex_app/lib/all_tools/assignments.py Outdated
Comment thread ex_app/lib/agent.py
Comment on lines -138 to +155
Today is {CURRENT_DATE}.
Today is {CURRENT_DATE}.{CURRENT_TIME}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt of passing just the date and the timezone since after some time in the chat session or picking it up again, the time would be off, so would the date but that wouldn't affect that many sessions, only the ones that span across days.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a concern, substituted inside call_model on every LLM round (and every turn of a resumed session), recomputes the current date/time anew

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah you're right, damn, but that's all the more reason to not include the time in the system prompt, it would bust the cache every second.
what do you think about adding a new tool for "date + time + user's timezone" instead?

(for later reference: https://github.com/nextcloud/context_agent/blob/ee5df89e3dcb71f05d2665647adf27d023f7fa96/ex_app/lib/agent.py #L212-L218)

@WSHAPER WSHAPER Oct 8, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did some digging: prefix caching needs byte identical prefixes, and dynamic timestamps in system prompts are a known cache killer in production, e.g. openclaw hit exactly this and paid 10x (openclaw#19534), the date itself is byte stable within a day though, so only the current time breaks the cache on every call.

Claude.ai itself uses "current date in the system prompt, injected once per conversation" (Claude system prompt docs, Claude 4 prompt analysis) plus an official time MCP reference server for precise time (modelcontextprotocol/servers/src/time), and the general rule is stable content before variable content with cache points after the stable part (AWS Bedrock prompt caching).

what do you think about adding a new tool for "date + time + user's timezone" instead?

So i'd follow that pattern: keep the date in the prompt, drop the time, add a small get_current_datetime tool. "tomorrow at noon" needs the date and the tz, not the minute.

…f requiring it

Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com>
Comment thread ex_app/lib/all_tools/calendar.py Outdated
Comment thread ex_app/lib/all_tools/calendar.py Outdated
Comment thread ex_app/lib/all_tools/calendar.py Outdated
Comment thread ex_app/lib/all_tools/assignments.py Outdated
Comment thread ex_app/lib/all_tools/assignments.py Outdated
Comment thread ex_app/lib/all_tools/calendar.py

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add_task and update_task might also required the timezone checks.
would also be nice to include "Omit start_time and end_time parameters to create an all-day event." in the prompt as schedule_task defines.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add_task and update_task do the check after the fallback, the omit line is already in the schedule_event description (calendar.py:133), or did you mean somewhere else?

Comment thread ex_app/lib/all_tools/assignments.py Outdated
…pper-level tz check

Review round 2: the tz helpers move to all_tools/lib/tz.py
(get_timezone, user_timezone, starts_at_timestamp) so calendar and
assignments share one implementation, with type hints. Optional
parameters carry explicit None defaults everywhere the agent can omit
them. The async wrappers for schedule_event/add_task/update_task
called the validation on the raw parameter, before the profile-timezone
fallback in the sync bodies, so an omitted timezone errored even when
the profile had one; the wrappers now only delegate. The scheduled-task
starts_at docstring drops the stale find_details_of_current_user hint
since the timezone defaults to the profile.

Assisted-by: opencode:zai/glm-5.3
Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

schedule_event converts Europe/Berlin to UTC instead of preserving TZID Improve timezone handling

2 participants