The 30s time-to-first-token guard measured from the turn's first model call
to the first answer token, so thinking, tool execution and the second call's
prefill all counted against it. Long reasoning on a long conversation was
cancelled mid-generation (llama-server: stop: cancel task at n_gen=1287).
- stream_query: the timeout now fires only when an in-flight model call goes
silent for TTFT_TIMEOUT_SECONDS. Any chunk (thinking or empty) resets the
clock, each model call starts a fresh one, and time spent in tools between
calls doesn't count.
- Trim replayed history (blueprints/conversation/context.py): tool results
and user messages from earlier turns are capped at 4000 chars (attachments
included) and their images replaced by a placeholder. The current turn is
untouched. The window no longer starts with orphaned ToolMessages.
- Log per-call prefill time, duration and prompt/output tokens
(stream_usage=True) plus max_prompt_tokens in the stream_request line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- extract_text (PyMuPDF for PDFs) now runs via asyncio.to_thread in both
/upload and stream-query, so a large PDF doesn't stall other streams.
- /upload returns 422 when extraction yields no text: a scanned PDF
without a text layer, or an empty text file.
- Mock server: *scan*.pdf uploads return that 422.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /api/conversation/upload takes an image (via process_image) or a
PDF/txt/md/csv file, checks the conversation belongs to the caller, and
stores files at conversations/<id>/files/<uuid>.<ext> with the original
name in S3 metadata. Anything else gets a 415.
stream-query accepts file_key (must sit under this conversation's
files/ prefix), extracts its text capped at 12k chars, appends it to the
agent's user turn as <attached file> and records 📎 name on the saved
message.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Convert ynab_budget_summary, ynab_search_transactions, ynab_spending_report
and ynab_category_budget to content_and_artifact (ynab.summary,
ynab.transactions, ynab.spending_report, ynab.category_budget). The text
the LLM sees is unchanged. Not-synced, bad-input, empty and error paths
return no artifact; Decimals become floats and dates ISO strings.
ynab_search_transactions now fetches up to 500 rows in one query (with an
id tie-break so ordering is stable) and lists the first `limit` in the
text; the artifact carries all of them. ynab_account_balances,
ynab_recent_changes and ynab_sql stay text-only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Convert mealie_get_recipe, mealie_meal_plans, mealie_todays_meals,
mealie_shopping_lists, obsidian_read_note, journal_get_today,
obsidian_search_notes and simba_search to content_and_artifact. The text
the LLM sees is unchanged; the artifact is a JSON-safe ToolArtifact dict
per the Cowork UI contract (mealie.recipe, mealie.meal_plan,
mealie.shopping_lists, obsidian.note, search.documents). Error, empty and
not-configured paths return no artifact.
simba_search's artifact changes from a list of Documents (unused) to a
search.documents dict.
New optional env vars: MEALIE_GROUP_SLUG (default "home") for Mealie v2
recipe links, OBSIDIAN_VAULT_NAME for obsidian://open links.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve config/db.py (artifacts + ynab models) and regenerate the YNAB
migration with aerich as 18 on top of main's 17_add_artifacts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the Makefile .PHONY conflict (desktop + ynab targets) and
regenerate the YNAB migration with aerich as 17 on top of main's
16_add_oidc_states, replacing the colliding 16_add_ynab_mirror.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Failure backoff is re-checked under the sync lock, so parallel tool
calls share one failed YNAB request instead of retrying in turn.
- Budget id: YNAB_BUDGET_ID aliases ("last-used", "default") count as
unset; state is always keyed by the real budget id, a mismatched
response errors instead of silently skipping every sync, and more
than one stored budget is an error rather than an arbitrary pick.
- Change log renames only id fields that resolve to names (account,
payee, category, group); subtransaction transaction_id stays as is.
- ynab_recent_changes: correct singular for "categories", multi-word
types, and the "month" type.
- The SQL wrap test now parses the wrapped query.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Full resyncs mark mirror rows missing from the download as deleted;
YNAB omits deleted entities from non-delta responses, so deletions
made just before a full sync never reached the mirror.
- YNAB tools return errors as text instead of raising, since ToolNode
re-raises and would fail the whole chat turn. ynab_sql catches all
sqlglot errors (e.g. an unbalanced quote) and runs the SQL regenerated
from the validated tree, which also drops comments that broke the
LIMIT wrapper. Results keep duplicate column names.
- Sync fetches from YNAB before opening the DB transaction, discards the
payload if the other container synced meanwhile, re-checks freshness
under the lock so parallel tool calls share one sync, and backs off
for 60s after a failure.
- The mirror is pinned to one budget; a changed YNAB_BUDGET_ID errors
instead of mixing budgets, and an unset id stays on the budget the
first sync resolved from "last-used".
- Change log: no entries for empty month rollover rows, and id fields
are logged as names (category, payee, account). ynab_recent_changes
accepts the names it prints ("assignment") and plurals.
- Outputs sized to the 4000-char tool result cap; ynab_sql reuses the
shared admin message.
- YNAB API client moves back to utils/ynab_service.py, and the frontend
tool status labels list the new tools.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
asyncpg raises OperationalError (not ValueError) on malformed UUIDs, so
bad artifact ids now 404 and a bad conversation_id 400s before any query.
get_conversation keys artifacts by str(message_id) so matching doesn't
depend on the driver's UUID types.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stream_query emits the new tool_start/tool_end payloads, maps
on_tool_error to tool_end status=error, and saves an artifact linked to
the tool message. GET /api/conversation/<id> adds tool, label and
artifact to tool messages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /api/artifacts?conversation_id= lists ArtifactRefs without data;
GET /api/artifacts/<id> returns the full artifact. Both 404 for other
users. create_artifact_from_output persists ToolArtifact dicts and drops
ones over 256 KB serialised.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pure functions build tool_start/tool_end payloads with run_id, friendly
label, truncated input (config dropped), status, duration and artifact ref.
Labels cover every agent tool, both old and #55 YNAB names, and the
homebox_/bookstack_ MCP prefixes, falling back to "Using <tool>…".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add transitional @jwt_access_required (accepts access tokens, and for now
also refresh tokens) and use it on every protected route and inside
admin_required. /api/user/refresh stays refresh-only.
- Move pending OIDC state/PKCE verifiers from an in-process dict to an
oidc_states table (10 min TTL, single-use) so logins survive a blue/green
flip. Drop the unused redirect_after_login.
- Unit tests for the decorator and the state store.
Deploy this commit on its own, before the access-token frontend: the old
backend answers access tokens with 422, so the new frontend only works once
this backend is live on both colors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
YNAB has no event feed, so keep a local copy in ynab_* tables synced via
server_knowledge delta requests: full download on first run, deltas every
YNAB_SYNC_INTERVAL seconds and on demand before each tool call, full
resync daily. Each sync diffs against the mirror and appends real edits
(transactions, assignments, renames) to ynab_changes, the edit history
YNAB doesn't keep.
Agent YNAB tools now answer from the mirror: budget summary (spending
computed from transactions, since YNAB's month activity nets in credit
card payment categories), account balances, transaction search with
splits expanded, spending reports by category/payee/account/month,
category budget, and recent changes. Adds an admin-only read-only
ynab_sql tool guarded by a sqlglot allowlist (scope-resolved tables, no
dangerous functions), a READ ONLY transaction, a timeout, and a row cap.
Replaces utils/ynab_service.py and drops the unused ynab SDK.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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>