Skip to content

Infer event duration instead of defaulting every event to 30 minutes - #33

Open
greatxp117 wants to merge 33 commits into
moeadham:mainfrom
greatxp117:feat/infer-event-duration
Open

Infer event duration instead of defaulting every event to 30 minutes#33
greatxp117 wants to merge 33 commits into
moeadham:mainfrom
greatxp117:feat/infer-event-duration

Conversation

@greatxp117

@greatxp117 greatxp117 commented Aug 19, 2026

Copy link
Copy Markdown

The problem

Most forwarded emails give a start time but no end time. When that happens, every event lands on the calendar as DEFAULT_EVENT_LENGTH_MINUTES (30), so a dinner reservation and a standup get the same block. Start times are usually right; end times usually aren't.

The root cause isn't really the constant — it's the prompt. All five few-shot examples in getEventData showed end_time: undefined for events with no stated end time, which trains the model to leave it blank whenever the email doesn't spell it out. That's the common case, so the fallback fires almost every time.

The change

1. Ask the model to infer a duration (prompts.ts). Stated end times and durations are still used verbatim. When there isn't one, the prompt now asks for a realistic end_time based on what kind of event it is, anchored by a short table of typical durations (coffee 30m, doctor 1h, dinner 2h, wedding 4h, and so on), with instructions to adjust for agenda length, attendee count, and distance. The five examples now show inferred end times consistent with that table. end_time is still left undefined for events that are genuinely all-day or open-ended, or that have no start time.

2. Better fallback when nothing can be inferred (calendarHelper.ts). Google Calendar exposes the user's own defaultEventLength setting, which is what Google itself uses for events created without a duration — a better fallback than a hardcoded constant. It's fetched alongside the existing calendarList.list() call via Promise.all, so it adds no latency, and it needs no new OAuth scope (the app already requests full auth/calendar). DEFAULT_EVENT_LENGTH_MINUTES stays as the last resort if the setting can't be read.

The ICS path is untouched — those events already carry real end times.

3. Show the end time in the confirmation email (eventHandler.ts). Inferred durations are only useful if you can see them, and the reply printed the start time alone. A new formatEventDateRange helper, used by both the single-event and multi-event replies, prints the range: same-day events collapse to one date (Thursday, August 20th, 2026 at 6:00 PM - 8:00 PM CDT), multi-day events spell out both ends, and it falls back to the old start-only string if an event has no end time.

Testing

Adds UT12.5, which forwards a restaurant confirmation with a start time and no end time, and asserts the created event is longer than an hour and at most four. The bounds are loose on purpose so it's a regression guard against the flat default rather than a brittle assertion about one model's exact output.

npm run lint is clean. npm run build has 9 pre-existing @types/express errors in routes.ts/drive/routes.ts on main — same 9 before and after this branch, none in the files touched here.

I wasn't able to run the full suite end to end (it needs live Google OAuth plus Resend and OpenRouter keys), so UT12.5 is unverified against the real model — worth a run before merging.

🤖 Generated with Claude Code

moeadham and others added 30 commits November 5, 2025 14:02
Still needs a lot of work, will largely switch to resend as well
requires serious code review
also use ZOD for schemas, and enforce them
resend's timeout is too low; respond to callback immediately and dispatch the work

This is the way
* test: extend auth timeout to 60 secs

* fix: move public access tokens to env file to pass github screening

* refactor: convert to typescript

* refactor: reorganize code & minor linting fixes

* refactor: move to auth module

* chore: remove duplicate gitignore

* feat: retry logic for OpenRouter / OpenAi client

* chore: revert to firebae-functions v7

* refactor: linting updates

* fix: set to new max timeout firebase fn limit (1800)

* feat: make MAIN_EMAIL_ADDRESS configurable

* chore: run build before deploying to firebase

* feat: filter to process callbacks only from the assigned email

* feat: updates to support fwd2cal-dev

* refactor: derive email addresses from MAIN_EMAIL_ADDRESS instead of hardcoding them

* test: tests should use MAIN_EMAIL_ADDRESS

* refactor: move to modular types

* fix: npm run lint now fully working & fix linting errors
jjacarillo and others added 3 commits March 2, 2026 15:22
* feat: process document attachments

* feat: better understanding of admin actions (skill-based config)

* test: enable emulator hosting port 5002

* chore: tidy up SKILL.md

* refactor: remove hardcoded numbers and make them config

* feat: skip attachments over 10MB

* fix: bug where pdf parsing last 2 pages instead of first 2

* feat: 50MB cap to total attachments size

* feat: initial version of drive agent

* fix: exclude embedded images

* fix: do not create duplicate folder

* fix: only excluded inline images

* fix: timezone detection -- Windows to IANA format

* fix: do not show misleading auth error email when an error occured when creating the event

* test: do not hardcode fw2cal app id

* refactor: isolate calendar and drive agents

* fix: remove unnecessary calendar access to drive agent

* refactor: reuse resend endpoints / functions between agents

* feat: fw2drive new flow

* fix: minor bug fixes and code cleanup

* feat(drive): process images (both attachments & inline)

* fix: images without meaningful text should be categorized as Photos

* feat: organize my drive proposal

* feat: minor improvements to move folder and drive skills match

* fix: skip inline headers, footers, and logos

* fix: duplicate / inconsistent folder when moving multiple files

* feat: improve prompt to organize file

* feat: consolidate organize drive summaries

* feat: execute organize drive with undo

* feat: use created date as prefix instead of modified date

* fix: handle drive processing post auth

* refactor: temporarily persist drive file proposals in Firestore

* feat: add approve and undo buttons
- reply to email still works
- buttons with hmac sign

* fix: handle other oauth failure / errors

* fix: separate users per agent

* refactor: move away from shared endpoints
- each agent will have isolated endpoints (auth, resend) but will reuse
  logic

* chore: deploy script to accept project id

* test: insufficient permissions

* refactor: clean up huge handlers and routes

* fix: separate resend signing secret for drive webhook

* feat: include event year in add event email

* chore: check for credentials json on deploy

* feat: redirect to error page when permissions are not granted on signup
- includes a button to try auth again

* fix: firebase hosting & rewrites

* fix: LengthFinishReasonError for event prompt
- revert to old schema fields
- set max for each field in zod schema
Most forwarded emails give a start time but no end time. Today every one of
those events lands on the calendar as DEFAULT_EVENT_LENGTH_MINUTES (30), so a
dinner reservation and a standup get the same block.

Two changes:

- The prompt now asks the model to infer a realistic end_time from the kind of
  event when the text doesn't state one, with a table of typical durations to
  anchor it. Stated end times and durations are still used verbatim. All five
  few-shot examples had `end_time: undefined` for events with no stated end,
  which was training the model to leave it blank; they now show inferred end
  times consistent with the table.

- When no end time can be inferred, fall back to the user's own Google Calendar
  "default event length" setting rather than a hardcoded constant. It's fetched
  alongside the existing calendarList.list() call, so it costs no extra
  latency, and needs no new OAuth scope. DEFAULT_EVENT_LENGTH_MINUTES remains
  the last resort if the setting can't be read.

Adds UT12.5, which forwards a dinner reservation with no stated end time and
asserts the resulting event is longer than an hour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Now that end times are inferred rather than defaulted, they're worth showing.
The confirmation email only printed the start, so there was no way to tell from
the reply whether the inferred duration was right without opening the event.

Adds formatEventDateRange, used by both the single-event and multi-event
replies. Same-day events collapse to one date with a start-end time
("Thursday, August 20th, 2026 at 6:00 PM - 8:00 PM CDT"); multi-day events
spell out both ends. Falls back to the old start-only string if the event
somehow has no end time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.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.

3 participants