Track feature ideas and implementation status here. Mark items [x] when implemented.
Forward any email to the bot's Val Town email address. Claude reads the email, takes action (add to calendar, create task, store info), posts a summary to the Telegram group, and replies to the sender confirming what was done. The forwarder can add instructions at the top of the email body (e.g. "add this to calendar" or "just FYI") — these are parsed out and followed.
Files: email.ts (Val Town email type)
Tasks now track an owner (jeff/marisa/both) and an optional reminder time. A new 15-minute cron (reminder-cron.ts) fires a proactive Telegram ping — with a real @mention — when a task's reminder time arrives. Owner→Telegram-ID mapping is auto-learned from message.from on every inbound message (no config required). Timed tasks ping at the exact specified time; date-only tasks ping at 8am ET. The update_task tool lets you assign or reschedule after creation.
This delivers the owner-tracking half of the Responsibility Model backlog item.
When the bot completes a simple, unambiguous action (adding a task, setting a reminder, adding a calendar event, storing info), it now acknowledges with an emoji reaction on the triggering message instead of writing a full sentence. Questions, lookups, and anything that needs a conversational response still get text replies.
How it works: A react tool in tools.ts calls setMessageReaction (Telegram Bot API 7.0+), attaching the emoji directly to the user's message. If Telegram rejects the reaction (group settings, old message), it falls back to sending the emoji as a plain message. The LLM decides which mode fits based on guidance in the system prompt.
Files: tools.ts (react tool + ToolCtx type), main.ts (setMessageReaction helper, updated processMessage, updated system prompt)
Today the bot only replies in the group when @mentioned (unless RESPOND_TO_ALL=true, which is noisy). @mention is reliable but cumbersome. Add a cheap, fast per-message LLM "gate" that decides whether the bot should respond when it wasn't tagged — because the message is directed at it, is a reply to one of its messages, continues a conversation with it, or is something it can clearly help with. Posture is conservative: only chime in when fairly clearly warranted, to avoid butting into ordinary chatter. A third mode between "@mention only" and "respond to all."
Approach — two-stage gate for non-mention group messages:
message.reply_to_message.from.is_bot) always gets a response — zero LLM cost.{respond, confidence, reason} via a forced tool call, gated by a confidence threshold (default 0.8 = conservative). Fail-safe: any error/parse failure ⇒ stay silent (matches today's default). Provider-flexible — reuses the existing isGptModel() branch so it works on a gpt-* or claude-* gate model.Implementation steps:
llm.ts — add config exports:
SMART_RESPONSES = (Deno.env.get("SMART_RESPONSES") ?? "true") === "true"GATE_MODEL = Deno.env.get("GATE_MODEL") ?? "claude-haiku-4-5" (cheapest; can point at any gpt-/claude- id)GATE_CONFIDENCE_THRESHOLD = Number(Deno.env.get("GATE_CONFIDENCE_THRESHOLD") ?? "0.8")llm.ts — add classifyShouldRespond({ text, history, botUsername }) => { respond, confidence, reason }:
respond: true only when clearly directed at the bot, clearly continuing a thread with it, or an unambiguous actionable request; quiet on chatter / messages aimed at the other person / ambiguity.decide (schema {respond: boolean, confidence: number, reason: string}):
tool_choice: {type: "tool", name: "decide"}, max_tokens: 128, parse tool_use.input.OPENAI_TOOLS_RESPONSES shape, forced tool_choice, max_output_tokens: 128, reasoning: {effort: "low"}, parse function-call arguments.{respond: false, confidence: 0}; console.log the decision + reason.[photo] so the gate stays text-only/cheap.main.ts — replace the gate at lines 784-788: if group && not RESPOND_TO_ALL && not @mentioned → respond free if replyToBot; else if SMART_RESPONSES run classifyShouldRespond and only proceed when respond && confidence >= GATE_CONFIDENCE_THRESHOLD; else legacy return new Response("OK"). Import the new symbols. Downstream (typing, photo download, processMessage) unchanged — the full model call only happens once a message clears the gate, same as @mentions today.SMART_RESPONSES (default true), GATE_MODEL (default claude-haiku-4-5), GATE_CONFIDENCE_THRESHOLD (default 0.8). Add to CLAUDE.md env table.changelog.md + CHANGELOG_LATEST in main.ts. No Telegram announcement (minor).Cost estimate: per check ≈ ~800 input + ~40 output tokens (model-independent — multiply by your gate model's rate). At the default Haiku 4.5 ($1 in / $5 out per MTok) ≈ $0.001/check:
| Volume (non-mention group msgs) | Checks/mo | Cost/mo |
|---|---|---|
| Light (~20/day) | ~600 | ~$0.60 |
| Moderate (~50/day) | ~1,500 | ~$1.50 |
| Heavy (~150/day) | ~4,500 | ~$4.50 |
Direct replies to the bot cost $0 (deterministic path). The gate is strictly new cost — it replaces silent drops with a ~$0.001 check; only passing messages hit the main model, same as @mentions today (~10–50× cheaper than a full response turn). Prompt caching won't help (stable prefix ~370 tokens is below the min cacheable prefix; volume is low). Note: MODEL is currently a GPT model whose current per-token rate is not quoted here — apply your OpenAI rate to the ~800-in/~40-out figures if keeping the gate on a gpt-* model.
Verification:
SMART_RESPONSES=true; confirm the gate model's API key is set.gate: respond=… conf=… — … to confirm/tune the threshold.RESPOND_TO_ALL=true still work unchanged.Store freeform text under a topic (e.g. "store these ideas for our 2027 ski trip"), distinct from the key/value fm_info table.
Implementation steps:
fm_notes table: id, topic, content, created_by, created_atinitDb() for the new tableaddNote(topic, content, createdBy) and searchNotes(query) db functionsTOOLS array in main.ts:
add_note(topic, content) — store freeform notes under a topic labelsearch_notes(query) — full-text search across topic and contentexecuteTool switchNatural-language weather queries: "what's the weather in Boston this weekend?"
Implementation steps:
get_weather(location, days_ahead?) tool to TOOLS arraygetWeather(location, daysAhead) using Open-Meteo (no API key required):
https://geocoding-api.open-meteo.com/v1/searchhttps://api.open-meteo.com/v1/forecastexecuteTool switchUpdate an upcoming event's title, start/end time, or description via natural language — "move the dentist appointment to 3pm" or "rename Mara's recital to Spring Concert".
Files: main.ts — findCalendarEventByQuery, updateCalendarEvent, update_calendar_event tool
Send a photo to the bot and Claude will describe it, extract text, read a receipt, parse a whiteboard, etc.
Implementation steps:
main.ts around line 468), detect message.photo in addition to message.textdownloadTelegramPhoto(fileId) function: calls getFile API then fetches the image bytesimage content block in the user messageadd_task items parsed from a photo of a handwritten list)message.caption)Lightweight regression testing for the LLM's tool-routing and response quality.
Approach: two layers
Layer 1 — Trace logging (production data, ~30min)
Add an fm_evals SQLite table that captures every real LLM interaction: input message, tools called, and final response. Zero test-writing upfront; surfaces failure patterns from real traffic. Add a /evals command or HTTP endpoint to browse the log.
Layer 2 — Golden test val (~1h)
Create evals.ts as a script val with ~8–10 fixed test cases that verify tool routing:
const cases = [
{ input: "Jeff: add milk to the grocery list", expect_tools: ["add_task"] },
{ input: "what's on the calendar this week?", expect_tools: ["get_calendar_events"] },
{ input: "remember the wifi password is Hunter2", expect_tools: ["store_info"] },
{ input: "mark task 5 as done", expect_tools: ["complete_task"] },
{ input: "hey how's everyone doing", expect_tools: [] }, // should NOT call tools
];
// For each: call Anthropic with the same system prompt + test input,
// assert tool_use blocks match expected tools, log PASS/FAIL to console + SQLite
Run manually or on a cron; results visible in Val Town logs.
Implementation steps:
fm_evals table: id, input, tools_called, response, created_at, passedprocessMessage, after each completed agentic loop, insert a row into fm_evalsevals.ts script val importing the same TOOLS and system prompt from main.tstool_use blocks to expected, write results to fm_evals/evals slash command in main.ts to show recent pass/fail countsExtend existing vision to automatically extract structured events and todos from photos — school flyers, birthday invitations, sports schedules, camp confirmation emails, etc.
The current image parsing describes what's in a photo; this goes further: Claude identifies dates, times, places, and required actions, then proposes adding them to the calendar or task list in a single step. The chat becomes the family inbox — you shouldn't need to decide whether something goes on the calendar or the task list.
Implementation steps:
add_calendar_event, add_task, or add_note"confirm_before_adding flag in the message handler so Claude can ask "Found: Mara's fall concert Nov 14 at 7pm. Add it?" before writingTrack things that need figuring out but have no date attached — distinct from tasks ("do this") and calendar events ("be there at X"). Examples: "we need to figure out summer camp", "decide on Mara's birthday party venue", "sort out ski rentals for Vermont trip".
Open loops persist until someone explicitly closes them. The bot surfaces them in the Sunday weekly briefing and when contextually relevant (e.g., upcoming trip → check open loops for that trip).
Implementation steps:
fm_loops table: id, title, context, status (open/closed), created_by, created_at, closed_atadd_open_loop(title, context?) — create an unresolved itemclose_loop(id_or_title) — mark resolvedlist_open_loops() — show all unresolved itemsTwo recurring messages that turn the bot from reactive to proactive:
Morning briefing (daily, ~7am): "Today: Mara school 8:15; Cass daycare; Jeff has late meeting until 5:30; rain at 3 — bring raincoats. Mara needs her library book today."
Sunday week-ahead (~8am): Reconciles both calendars + memory + open loops. Doesn't just list events — answers "what does this week require us to do?" Example output: "Mara's birthday party Saturday → gift not bought yet; Jeff out Thursday evening → Marisa solo bedtime; ski weekend coming → Cass rentals unresolved."
Implementation steps:
cron.ts (or extend existing) with two interval jobsfm_info/fm_notessendTelegramMessage/briefing on|off slash command stored in fm_infoA richer layer on top of fm_info for the facts family life constantly needs: kids' clothing/shoe sizes, school schedules, babysitter contact info, gift ideas, activities the kids love, dietary restrictions, packing preferences, places you've stayed, things Marisa or Jeff have mentioned wanting.
The goal: natural queries work years later. "What size shoes does Mara wear?" "What was that restaurant in Rotterdam the kids loved?" "What are gift ideas for Cass?"
Implementation steps:
fm_memory table: id, person, category, key, value, created_at, updated_at
person: "mara", "cass", "jeff", "marisa", "family"category: "sizes", "gifts", "activities", "places", "contacts", "preferences"remember(person, category, key, value) — store a memoryrecall(person?, category?, query) — retrieve matching memoriesfm_info usage — migrate key entries on first runMaintain a "trip object" in memory for each upcoming family trip. Tracks logistics as a checklist: lodging, tickets/passes, rentals, restaurant reservations, packing list. A few days before departure, auto-generates a packing list based on destination + weather forecast + planned activities + family ages.
Implementation steps:
fm_trips table: id, name, destination, start_date, end_date, status, notesfm_trip_items table: id, trip_id, category (lodging/transport/activity/rental/other), description, status (open/done), ownercreate_trip(name, destination, start_date, end_date) — create trip recordadd_trip_item(trip_name, category, description, owner?) — add a checklist itemget_trip_status(trip_name) — show full trip with open/done itemsstart_date: auto-send trip status + generated packing list (via cron) using weather forecast for the destinationShare any URL and the bot reads it — school newsletters, event pages, Google Docs, Google Sheets. Claude fetches the content and proactively extracts dates, tasks, and important info.
Files: tools.ts (fetch_url tool + fetchUrl + stripHtml), main.ts (system prompt, /start, /help updated)
Let the bot look things up: local events, restaurant hours, camp registration dates, activity options, nearby attractions. Turns "remember and coordinate" into "figure this out for us."
This is the unlock for: "find a good Italian restaurant near us for Friday", "when does [summer camp] registration open?", "what's open on Columbus Day weekend?", "find something fun to do Saturday with a 5-year-old."
Implementation steps:
web_search(query) tool using a search API (Brave Search or Tavily — both have free tiers; store key in Val Town env)BRAVE_SEARCH_API_KEY or TAVILY_API_KEY to Val Town env varsOn Friday afternoons when the weekend has unscheduled time + good weather, the bot proactively sends outing ideas — drawing on family memory (what activities the kids like, places you've been) + weather + calendar gaps.
Also: flag weather-sensitive plans ("rain forecast Saturday — your park plan might need a backup"), first snow ("sledding Sunday?"), or local events (concert, festival) matching the family's interests.
Implementation steps:
fm_notes topic: "recent outings")Track who owns what for a given event: preparation, transportation, and follow-through. When both parents are busy at the same time, the bot flags the conflict rather than leaving it implicit.
Example: "Mara's birthday party Saturday 2pm — Jeff driving, gift still needed, card still needed." Or: "Jeff is out Thursday evening → Marisa is solo bedtime — confirm?"
Implementation steps:
owner field to calendar events and tasks (values: "jeff", "marisa", "both", "tbd")assign_owner(event_or_task_id, owner) — set responsibilityowner = tbd or no owner, flag it explicitly