Store Beli's category code on each ranking so the agent can filter by type
(restaurant/bar/cafe/bakery). Category label shown in tool output.
Requires migration for beli_category column + updated unique constraint.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds friend_beli_user_id (default '') and friend_name to beli_restaurants,
updates unique constraint to include friend_beli_user_id.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each nightly profile refresh now also picks FRIEND_SAMPLE_SIZE (5) random
followed users and fetches their rankings, building up a picture of friends'
lists over time. Rows are tagged with friend_beli_user_id/friend_name so the
agent tool can surface attribution ("via Alex").
Requires migration to add friend_beli_user_id/friend_name columns and update
unique_together on beli_restaurants.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Category codes: RES/DES/BAR/BAK (was full English words)
- score field: use 'value' from Score model (was 'score', which doesn't exist)
- rank_index: pass array position (no rank field in response)
- fetch_bookmarks: no query params (user param caused 405)
- notes: correctly None (stored separately, not in ranking results)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bookmarks returning 400 was aborting the whole refresh and discarding
successfully-fetched ranking data. Now bookmarks log a warning and
continue. Also pass user= to the bookmark endpoint in case it's required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tortoise calls to_python_value() when setting field values at create() time,
not only when loading from DB. Add _is_fernet_token() to distinguish encrypted
DB values (≥73 decoded bytes, version 0x80) from plaintext being set — the
latter passes through untouched while wrong-key errors on real tokens still raise.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Multi-tenant system to fetch and store restaurant lists from Beli accounts.
Each user can connect up to 3 Beli profiles; a nightly Cronicle job (1–5 AM,
randomised) refreshes the 3 stalest profiles using the reverse-engineered Beli
API (mirrored at git.torrtle.co/ryan/beli-api). Rankings (been) and bookmarks
(want-to-try) are stored in Postgres and queryable via a new LangChain agent
tool. Beli passwords and refresh tokens are stored Fernet-encrypted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reusing the message/job ID as thread_id causes corrupted checkpoint state
when a previous run failed mid-tool-call. A fresh UUID each time guarantees
a clean conversation context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If scheduled_at has sub-minute seconds (e.g. retry = now+30s), the
Cronicle event would fire at the start of that minute before the message
is due and find nothing. Round up so it always fires after the due time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The shell script's Node.js socket timeout was 10s, too short for agent
runs with use_agent=true. Bump to 300s (5min) and set Cronicle job
timeout to 360s to match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of an every-minute poller, each scheduled message gets its own
Cronicle event created at schedule time:
- One-time: year/month/day/hour/minute one-shot
- Daily/weekly/monthly: recurring timing matching the recurrence setting
Retry (PUT with new scheduled_at on failed messages) also creates a new
Cronicle event and resets status to pending. Recurring messages reuse
their existing Cronicle event across occurrences.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The checkpointer requires a thread_id in configurable. Use stable IDs
derived from the message/job ID so each has its own checkpoint thread.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switches from curl (shell not available in Cronicle container) to a Node.js
inline script using the built-in http module. Tries raggr-green first, falls
back to raggr-blue automatically — no manual update needed on color swap.
Delete the existing broken Cronicle event before redeploying.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch shellplug → urlplug to avoid spawn errors in shell-less containers
- Explicitly set timing to all 60 minutes (Cronicle {} defaults to hourly :00)
- Restore RAGGR_INTERNAL_URL env var for the HTTP plugin target URL
Delete the existing broken Cronicle event before redeploying so it gets recreated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shell script falls back to the other color if the primary is down,
so the trigger works regardless of which container is currently active.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On startup raggr calls the Cronicle API (CRONICLE_URL + CRONICLE_API_KEY)
and creates a 'Scheduled Messages Trigger' shell event if it doesn't exist,
replacing the need to configure it manually in the Cronicle UI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the asyncio background loop (15s polling) with a POST
/api/scheduled-messages/trigger endpoint authenticated by X-Cron-Secret,
matching the existing cron jobs blueprint pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Quart exits the request/app context before iterating the SSE response
body, so current_app inside event_generator() raised "Not within an app
context". Capture the reference before the generator is defined. Also
add return after the exception error yield to prevent falling through to
a second "No response generated" event on the same stream.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add get_category_allocations() to YNABService that fetches all categories
grouped by category group (using the categories API for group structure),
showing assigned and available amounts for every non-hidden category.
Add the corresponding ynab_category_allocations agent tool so the LLM
can answer questions about per-category budgets even when a category has
no spending activity this month.
Also remove the [:10] cap in ynab_category_spending so all spending
categories are returned — previously categories ranked below 10th by
activity (e.g. Coffee slush fund) were silently dropped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LangGraph passes the full agent state dict {"messages": [...]} to the
prompt callable, not just the messages list. Iterating a dict yields
its keys, so "messages" (the string) was getting appended to the LLM
input — causing the agent to respond as if the literal word "messages"
was the user's query.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uses prompt=_build_prompt (a RunnableLambda) so LangGraph passes the
runtime config through, allowing per-request system_prompt injection.
Restores user memory loading for web and iMessage plain-text enforcement.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
email, scheduled_messages scheduler, and cron blueprints still imported
main_agent directly from agents. All now use current_app.main_agent so
they read the initialized value set by build_agent() in the lifespan.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Global module variable was unreliable across import boundaries. Storing
the compiled agent on app.main_agent and reading it via current_app is
the idiomatic Quart pattern and guaranteed to reflect the initialized value.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
from-import captures the None value set at module load; importing the module
and accessing .main_agent at call time gets the value set by build_agent().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
state_modifier was removed in this version of LangGraph; use the prompt
parameter with a static system prompt string instead. Dynamic memory
injection via configurable is deferred — memories are still saved by the
save_user_memory tool, just not pre-loaded into the system prompt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
langchain.agents.create_agent does not accept state_modifier or checkpointer;
those are LangGraph-only params available on create_react_agent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces per-request history reconstruction with a persistent AsyncSqliteSaver
checkpointer keyed by conversation ID, so agent state (tool calls, intermediate
steps) carries forward between turns without manual message list building.
Also includes the missing migration for the CronJob model added in bb4619a.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a new cron blueprint with UUID-keyed jobs that Cronicle triggers via
HTTP POST with a shared secret header. Also adds scripts/migrate-new.sh so
migrations can be generated locally against a Docker postgres when no prod
container is running.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the gws CLI subprocess approach with google-api-python-client.
Adds get_calendar_events, create_calendar_event, gmail_search_emails, and
gmail_get_email as LangChain tools; all admin-gated and auto-enabled when
token.json is present. Includes uv-runnable scripts/google_auth.py for
one-time OAuth2 setup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move LLM invocation into a background asyncio task so the webhook returns 200
before SendBlue's timeout triggers retries. Validation, conversation setup, and
user message persistence still happen synchronously before the ack.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Native reply threading: inbound replies continue their conversation via
SendBlue message_handle lookup; standalone messages create a new conversation
- Our responses are sent with reply_to so they thread visually in iMessage
- Typing indicator fires before agent invocation and stops in a finally block
- Add extract_agent_text() to handle list-type AIMessage content and strip
<think> tokens from Qwen3 — applied to iMessage, WhatsApp, and web chat
- ConversationMessage gains a nullable message_handle field (migration needed)
- /threads command lists recent iMessage conversations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tracks end-to-end response time and request counts (by stream mode and
result) via prometheus_client. Exposes GET /metrics behind a bearer
token (METRICS_TOKEN env var) so Prometheus can scrape it without
exposing raw data publicly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The iMessage webhook called main_agent.ainvoke without a config, so
save_user_memory always raised KeyError('user_id') when invoked. The
system prompt instructs the agent to save memories proactively, causing
repeated tool failures and ultimately returning the error fallback to
the user on most messages.
Fix:
- Pass agent_config with user_id/is_admin to ainvoke, matching the web
endpoint.
- Extract _build_system_prompt_with_memories out of the conversation
blueprint __init__ into memory.py (public name) so both the web
endpoint and iMessage share the same implementation, giving iMessage
users access to their saved memories.
- Remove the leftover debug print() in add_message_to_conversation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The ThinkingBubble was rendering after the messages list, so it appeared
below the partial answer while streaming. Inject it inside the messages
map just before the last streaming simba message so it sits above the
answer text. Keep the post-list fallback for when thinking is active
but no content has arrived yet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Route <think>…</think> content from the llama-server stream into
thinking_delta SSE events instead of discarding it. A small look-ahead
buffer in _route_content() handles tags split across chunk boundaries.
Strip thinking tags from the persisted final_message so the DB stores
only the actual response.
Frontend: ThinkingBubble component accumulates the live reasoning text
in a scrollable panel that auto-collapses to "Thought for Xs" once the
first content token arrives. Falls back to the existing heartbeat
behaviour for models that don't emit <think> tags.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Log the first non-empty additional_kwargs seen during the pre-content
thinking window to confirm whether llama-server surfaces reasoning_content
for qwen3. Will be removed after verification.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When llama-server is queue-backed it accepts the connection but never
produces tokens; after ~60s Caddy drops the connection and the frontend
clears loading state with no response and no error shown to the user.
Backend: replace the async-for event loop with an explicit __anext__()
iterator wrapped in asyncio.wait_for(). A 30s timeout (TTFT_TIMEOUT_SECONDS)
applies only in the window between on_chat_model_start and the first content
token — tool calls are excluded. On timeout, emits a stalled error event and
logs it rather than waiting for the proxy to kill the connection.
Frontend: separates stall errors from generic errors; shows a "Simba didn't
respond in time. Retry" pill that re-sends the original query.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>