Allowlist homebox-mcp's general item editor so the agent can rename items
and edit description, notes, quantity, purchase info, flags and custom
fields. Only passed args change; the rest of the item is preserved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Load an allowlisted subset of bookstack-mcp tools (search/read plus
create/update books, chapters, pages and comments; no deletes) over stdio
and add them to the admin agent alongside Homebox. Extract the shared
allowlist + is_admin wrapping into utils/mcp_tools.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Load an allowlisted subset of homebox-mcp tools (read + safe writes, no
deletes) over stdio and compile them into a separate admin agent, so
non-admins never see them. Each tool also re-checks is_admin per call.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces random.sample with a staleness-ordered pick: friends never
fetched sort first (epoch timestamp), then oldest fetched_at. Ensures
full following list coverage over successive nightly runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LangChain was sending reasoning_effort=thinking which Qwen3's chat
template rejects (only xhigh/medium/low are valid). low disables
thinking mode, which is appropriate for agentic tool use and saves tokens.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removed verbose "Use this tool when the user asks about" bullet lists
from all 31 tools. Each tool now has a 1-2 line description plus compact
arg docs. Saves ~3-5k tokens per request on a 32k context budget.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The LangGraph checkpointer replays full history on every request. Without
trimming, long conversations compound past the 32k llama-server slot limit.
12 messages covers ~4-6 turns including tool call/result pairs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously ordered by friend_beli_user_id first, returning 15 results
from whoever sorts first. Now fetches top-ranked rows globally and
caps at 3 per friend, giving a diverse cross-friend sample.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces include_friends with friends_only so the LLM can return
exclusively friend-sourced rows when user asks what their friends like,
rather than mixing in own rankings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
50k+ token requests were blowing past the 32k llama-server context window
when returning the full ranked list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /api/beli/profiles/<id>/refresh-friends triggers friend ranking
sampling on demand without waiting for the nightly cron. Also tightens
the beli_search_restaurants docstring so the LLM passes category=restaurant
when the user asks for restaurants, preventing bars/bakeries from appearing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add friend_beli_user_id="" to own ranking lookups to prevent
MultipleObjectsReturned when friend rows exist for same restaurant
- Move friend sampling out of refresh_profile into _refresh_after_delay
so manual refreshes stay fast; nightly cron still samples friends
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>