Skip to content

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

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

WSHAPER wants to merge 10 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 Outdated
…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
Comment thread ex_app/lib/all_tools/calendar.py
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>
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/agent.py Outdated
Comment thread ex_app/lib/agent.py Outdated
Comment thread ex_app/lib/agent.py
Comment thread ex_app/lib/all_tools/calendar.py
Comment thread ex_app/lib/all_tools/calendar.py
Assisted-by: opencode:zai/glm-5.3
Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com>
Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com>
Assisted-by: opencode:zai/glm-5.3
Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com>
Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com>
Assisted-by: opencode:zai/glm-5.3
Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com>
@WSHAPER

WSHAPER commented Oct 12, 2026

Copy link
Copy Markdown
Author

round 3 @kyteinsky

  • all-day events now emit DTSTART;VALUE=DATE with dtend on the following day, the assistant chat on my instance ran and read the event back over caldav: DTSTART;VALUE=DATE:20261013 / DTEND;VALUE=DATE:20261014
  • let GLM 5.3 add a payload test pinning both the all-day and timed shapes, no server needed, if you'd rather keep tests out of this PR I'll drop it
  • timezone is back at the end of update_task, docstring order matches the args again
  • prompt is date-only now, tz stated next to the date, time moved to a small get_current_datetime tool (picks up the default-on seeding on re-enable, so no admin step)
  • the /cloud/user lookup is cached per user with timed_memoize(300), profile changes still propagate within 5 min

@kyteinsky kyteinsky left a comment

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.

the changes look good, thanks!
a few nits below but all good otherwise.
opened an issue for the flaky tests here: #269

Comment thread ex_app/lib/all_tools/calendar.py
Comment thread ex_app/lib/agent.py
Comment thread ex_app/lib/agent.py Outdated
Comment thread tests/test_schedule_event_payload.py
@WSHAPER

WSHAPER commented Oct 14, 2026 •

Copy link
Copy Markdown
Author

@kyteinsky Thanks again for the review!

FYI: I've opened another can of worms that surfaced after inspecting what would be optimal for conversation branching:

In light of that, we'd need to discuss how we'd like to proceed.
To avoid scope creep, I'd like to defer the nitpicks for now.

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