Files
Maven/docs/plans/16-ambient-calendar.md
T
claude 58635f1a69 docs: decide the ambient calendar path — keep it, change the contract (V-432)
The task's confirmed defect is out of date. 4e4c917 added day words and a
past-grace refusal, so "завтра в 15:00" dates correctly, and 45a5e37
(V-482, this week) fixed a zone bug the task did not know about. What is
left is explicit dates ("5 августа"), which fail safe by being dropped
rather than stored on the wrong day. The task's third question also has
an answer: both readers hedge, plan.go:174 prefixes "похоже, ".

Everything else hangs on one question that this repo cannot answer, so
the doc names it as his: can the relay app read Android's calendar
provider, or only the notification text? A NotificationListenerService
sees a title and a body and cannot know a meeting's real start, so if
that is all there is, free-text parsing here is not a choice. If it can
read CalendarContract, the parser stops being necessary and nothing is
inferred at all. Reading the phone's calendar does not break the design
constraint, which is about holding a work credential on the homelab.

Decision: keep the endpoint, make a structured event the primary shape,
keep the free-text parse as the degraded path, delete only if the relay
is not being built. And do not patch the date parser first — that is the
patch the task explicitly refuses as closure, and it is the wrong order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 06:03:56 +04:00

4.7 KiB

Plan: What the ambient calendar path should be

The decision Vikunja #432 asked for. Written 04-08-2026.

Verdict: keep the endpoint, change the contract. The relay app sends structured fields, not a notification blob. The free-text parser stays as the degraded path, because there is a real case where the phone cannot produce structure. Delete the endpoint only if the answer to the one open question below is no.

First, the task's premise is out of date

#432 states as confirmed that every ambient event lands on the day the notification was posted, because there is no date parsing at all. That was true when the task was filed and it is not true now.

4e4c917 added dayWords and dayOffset (internal/calendar/ambient.go:53), so "завтра в 15:00" now dates to tomorrow. The same commit added ambientPastGrace, which refuses an event landing more than two hours before the notification, on the reasoning that the day was inferred and a stale inference is wrong rather than late. 45a5e37 (#482, this week) fixed a second dating bug the task did not know about: the wall clock was resolved against the notification's own zone, so every ambient meeting on a non-UTC box landed off by the deploy's UTC offset.

What is still missing is an explicit date. "5 августа в 15:00" and "12.08 15:00" carry no day word, so they date to today and the past-grace check drops them. That is a smaller defect than the one filed, and it fails safe rather than storing a wrong meeting.

The task's third question also has an answer, and the answer is yes. internal/morning/plan.go:174 prefixes an uncertain item with "похоже, " and cmd/mavend/actions_query.go:334 carries Confidence < 1.0 into the calendar recital. Both readers hedge.

The open question, and it is the owner's

Can the phone read Android's calendar provider, or only the notification text?

Everything follows from this and nothing in this repo can answer it.

A NotificationListenerService sees a title and a body. It cannot know a meeting's real start, end or organiser, because those are not in the notification. So if the relay is limited to the notification stream, free-text parsing on this side is not a choice, it is the only thing available, and #432's suggestion that the phone send structured JSON cannot be honoured.

If the app may instead read CalendarContract, it has the actual event rows, and the whole parser stops being necessary. That is the better shape by a wide margin: a real start and end, a real title, an explicit date, no clock-reading heuristic and no past-grace guard, because nothing is inferred.

Reading the phone's calendar provider does not break the constraint the design was built around. The refusal in internal/calendar/ambient.go:10 is about holding a work credential on the homelab, which is what ties the box's blast radius to the employer. The phone already holds that session. Nothing new lands on homesrv either way.

Assumption, and it needs his answer: a managed work profile may block a third-party app from reading work calendar rows. If it does, the notification stream is all there is.

The decision

Keep the endpoint. Deleting it costs the parser, the tests and the wg-facing token, and buys nothing while the question above is open. It is off unless -ambient-token is set, so an unbuilt relay carries no surface today.

Make structured the primary shape. /api/ambient should accept an event with an explicit start, end and title, and store it without parsing anything. Confidence stays below 1.0 and the source stays ambient:notif, because the provenance claim is unchanged: this is the phone telling Maven what it sees, not Maven reading a calendar.

Keep the free-text shape as the degraded path. It is what a notification-only relay can send, and it is already written and tested.

Delete it instead if the relay is not going to be built. That is the one answer that closes this without code, and it is his to give.

What not to do

Do not add date parsing to the free-text path yet. That is the patch #432 explicitly refuses to accept as closure, and it is the wrong order: if the relay can send a date, no date parser is needed, and if it cannot, the parser is guessing at a date from text that was never meant to carry one.

Follow-on, unrelated to the decision

cmd/mavweb/ambient.go:33 records a known gap worth keeping visible: an ambient meeting writes calendar_event_* and never calendar_busy, so it is good enough to recite and not good enough to suppress a nudge. That is backwards. Suppressing a nudge is the lower-risk use of a low-confidence signal, and reciting one is the higher-risk use. It needs an expiry on the busy level, so it is its own task either way.