Skip to content

PRD: /timebox — LLM-assisted next-day timeboxing #2

Description

@cristoforows

Problem Statement

As a Second Brain Bot user I brain-dump messages all day, but I have no low-friction way to plan tomorrow. My calendar only holds discrete events (meetings, appointments); it is useless for meticulous timeboxing of mundane tasks (dishes, gym, errands, deep-work blocks). Deciding when each small task happens — and which tasks to cut when the day overflows — is exactly the kind of judgment work I want to offload.

Solution

A /timebox command in the existing Telegram bot. The user enters a short collection session, sends tasks one per message (with optional freeform durations/constraints), and finishes with /done. An LLM then produces a complete timeboxed schedule for the next day: it parses each task, estimates missing durations, honors stated constraints, places everything inside the user's waking window, and — when the day can't fit everything — unilaterally drops the least important items and reports each drop with a reason. The schedule is returned as a formatted Telegram reply.

User Stories

  1. As a bot user, I want to type /timebox, so that the bot helps me plan a timeboxed schedule for tomorrow.
  2. As a bot user, I want to send tasks one per message during a timebox session, so that I can brain-dump my todo list quickly and naturally.
  3. As a bot user, I want each task message acknowledged with a 👍 emoji reaction (not a reply), so that I get confirmation without chat clutter.
  4. As a bot user, I want to optionally include duration and constraints in a task message (e.g. "deep work on report 90m, morning", "call mom after 6pm"), so that the schedule respects my intent.
  5. As a bot user, I want to omit duration for mundane tasks (e.g. "wash dishes"), so that the LLM estimates a sensible duration for me.
  6. As a bot user, I want to send /done to end collection and receive the generated schedule, so that the flow is a single ritual: dump → done → plan.
  7. As a bot user, I want the schedule to target tomorrow's date in my local timezone, so that "tomorrow" matches my actual day.
  8. As a bot user, I want a /timebox session started between 00:00 and 02:59 local time to plan the current calendar day instead of the next, so that late-night planning still targets the day I'm about to wake into.
  9. As a bot user, I want tasks placed within a default 09:00–22:00 waking window, so that the bot never schedules me during sleep.
  10. As a bot user, I want to override the waking window by saying so in a task message (e.g. "start my day at 7"), so that unusual days are accommodated without configuration.
  11. As a bot user, I want the LLM to drop tasks that don't fit the day and list each dropped task with a reason, so that I'm not forced into a back-and-forth deciding what to cut.
  12. As a bot user, I want the schedule reply in a consistent, scannable format (time ranges + task names, then a "Dropped" section), so that I can read it at a glance every day.
  13. As a bot user, I want to send /cancel during a session, so that I can abort without generating a schedule.
  14. As a bot user, I want a silent session to expire after 30 minutes with a notice, so that tomorrow morning's normal brain-dump messages are not silently swallowed as tasks.
  15. As a bot user, I want task messages sent during a timebox session to be excluded from my Google Drive daily log, so that my second-brain data stays free of context-less task fragments.
  16. As a bot user, I want /done with zero collected tasks to tell me nothing was collected and end the session, so that the bot never calls the LLM with empty input.
  17. As an unauthenticated user, I want /timebox to tell me to /authenticate first, so that the flow is consistent with every other bot feature.
  18. As the bot operator, I want /timebox gated behind Google authentication, so that strangers who find the bot cannot burn my OpenRouter credits.
  19. As a bot user, I want a failed schedule generation to keep my collected tasks buffered and tell me to retry /done, so that I never have to re-type my task list.
  20. As the bot operator, I want the LLM model, timezone, and late-night cutoff hour configurable via environment variables with sensible defaults, so that I can tune behavior without code changes.
  21. As the bot operator, I want the bot to fail fast at startup with a clear error if the OpenRouter API key is missing, so that misconfiguration is caught at deploy time, not mid-session.
  22. As the bot operator, I want /timebox to behave identically in polling mode and webhook mode, so that local development and production don't drift.
  23. As a bot user, I want /help and /start to mention /timebox, so that the feature is discoverable.
  24. As a developer agent, I want the scheduling logic in a module that accepts an injected LLM client, so that it can be tested offline with a fake LLM.

Implementation Decisions

Interaction flow (decided through design interview):

  • /timebox enters a multi-turn collection session implemented with python-telegram-bot's ConversationHandler. One task per message; /done ends collection and triggers generation; /cancel aborts; conversation_timeout of 30 minutes expires the session with a user-facing notice.
  • Each task message is acknowledged by setting a 👍 message reaction (Telegram set_message_reaction), not a reply.
  • The conversation handler must be registered before the catch-all save-to-Drive message handler, in both the polling entry point and the webhook entry point, so that session messages are diverted from the Drive pipeline. Handler registration is extracted into one shared function consumed by both entry points so they cannot drift.
  • The task buffer lives in per-user in-memory conversation state. Loss of the buffer on bot restart is accepted; the user re-runs /timebox.
  • /timebox checks the existing Google Drive authentication gate (same TokenStorage.is_authenticated check used by other handlers) even though v1 never touches Drive. The gate doubles as access control for paid LLM calls, and the same Google auth will later power calendar integration.

Target-day semantics:

  • Target date = tomorrow in the configured timezone (TIMEBOX_TIMEZONE, default Asia/Singapore), computed with Python's built-in zoneinfo.
  • If the session's /done happens between 00:00 and 02:59 local time (configurable as TIMEBOX_CUTOFF_HOUR, default 3, meaning hours strictly below the cutoff), the target date is the current day instead.
  • This cutoff is deliberately a separate config from the existing DAY_CUTOFF_HOUR used for Drive daily-file naming; the two concepts must not be coupled.

Scheduling behavior (LLM contract):

  • The LLM receives: the raw task messages (verbatim, order preserved), the target date, and the default waking window 09:00–22:00. It parses each message into a task with optional duration/constraints, estimates missing durations, honors freeform constraints (including window overrides stated by the user), and produces a non-overlapping timeboxed plan.
  • Infeasibility is resolved unilaterally: the LLM drops the least important / least time-pressured tasks until the plan fits, and every dropped task is reported with a one-line reason. No mid-flow questions back to the user.

LLM plumbing:

  • OpenRouter via langchain-openai's ChatOpenAI with the OpenRouter base URL — mirroring the pattern already used in the sibling second_brain_summarizer project. New dependencies: langchain>=0.3,<0.4, langchain-openai>=0.3,<0.4.
  • API key from OPENROUTER_API_KEY env var; model from TIMEBOX_LLM_MODEL env var, default deepseek/deepseek-v4-flash.
  • Structured output via .with_structured_output() bound to a Pydantic schema. The result contract (shape, not literal code):
    • schedule: list of items, each with start (HH:MM), end (HH:MM), task (string), optional note
    • dropped: list of items, each with task (string) and reason (string)
  • One automatic retry if the structured output fails to parse or the API call errors. After a second failure: user-facing error message, task buffer preserved, user may send /done again.

Module architecture:

  • A new deep scheduler module owns all scheduling logic behind a small stable interface: a target-date computation function (pure: now + timezone + cutoff hour → date), a schedule-generation function (tasks + target date + injected LLM → structured result), and a rendering function (pure: structured result → formatted Telegram text). The LLM client is injected so the module is testable without network access.
  • The Telegram conversation handler is thin glue: it owns session state and Telegram I/O only; all judgment lives in scheduler.
  • Config additions follow the existing Config class pattern (env var getter methods with validation, warnings, and defaults).

Output:

  • v1 output is the formatted Telegram reply only. Nothing is written to Drive or any calendar.

Testing Decisions

  • These are the first automated tests in this repository; introduce pytest and a tests/ directory. There is no prior art in-repo; the sibling second_brain_summarizer project has a tests/ directory that can serve as a style reference.
  • Good tests here assert external behavior only: given inputs, what comes out — never internal call sequences or private helpers.
  • Test the scheduler module only:
    • Target-date computation: before/after the 3am cutoff, across the midnight boundary, timezone-sensitive cases (e.g. server-UTC vs Asia/Singapore disagreement on what "today" is).
    • Rendering: a structured result with schedule + dropped items renders the expected Telegram text; empty dropped omits the section.
    • Generation: with a fake injected LLM — happy path returns a valid structured result; first-call parse failure followed by a valid retry succeeds; two consecutive failures surface an error while indicating the buffer should be preserved.
  • The Telegram conversation handler is explicitly NOT covered by automated tests in v1; it is verified manually in Telegram (session start, reactions, divert-from-Drive, /done, /cancel, timeout).
  • Config parsing tests are optional and low priority.

Out of Scope

  • Google Calendar integration (writing timeboxes to a calendar, reading busy slots) — explicitly the next iteration, reusing the existing Google OAuth.
  • Per-user timezone, waking window, or schedule preferences (any persistent user settings command).
  • Persisting the generated schedule to Google Drive or anywhere else.
  • Persisting the in-progress task buffer across bot restarts.
  • Editing or deleting individual tasks mid-session (user can /cancel and restart).
  • Handling edits to task messages after they were buffered (the originally-sent text is what gets scheduled).
  • Non-text task input (voice notes, photos).
  • Rescheduling/replanning flows ("move task X to 3pm").

Further Notes

  • Deployment requires adding OPENROUTER_API_SECRET-style secret (OPENROUTER_API_KEY) to the Fly.io app secrets before the feature works in production — a human step outside the code changes.
  • The 09:00–22:00 window and drop-aggressiveness live in the prompt; expect to tune the prompt after a few days of real use. Keep the prompt in one obvious place in the scheduler module.
  • Telegram message reactions require python-telegram-bot >= 20.8; the pinned version (22.6) supports them.
  • Commands inside a session (/done, /cancel) must be handled by the conversation's own states, not the global command handlers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentTriage: ready for an AFK agent to pick up

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions