Infer event duration instead of defaulting every event to 30 minutes - #33
Open
greatxp117 wants to merge 33 commits into
Open
Infer event duration instead of defaulting every event to 30 minutes#33greatxp117 wants to merge 33 commits into
greatxp117 wants to merge 33 commits into
Conversation
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
multimodal FTW
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
* 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>
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.
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
getEventDatashowedend_time: undefinedfor 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 realisticend_timebased 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_timeis 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 owndefaultEventLengthsetting, which is what Google itself uses for events created without a duration — a better fallback than a hardcoded constant. It's fetched alongside the existingcalendarList.list()call viaPromise.all, so it adds no latency, and it needs no new OAuth scope (the app already requests fullauth/calendar).DEFAULT_EVENT_LENGTH_MINUTESstays 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 newformatEventDateRangehelper, 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 lintis clean.npm run buildhas 9 pre-existing@types/expresserrors inroutes.ts/drive/routes.tsonmain— 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.5is unverified against the real model — worth a run before merging.🤖 Generated with Claude Code