Shared runtime
- One agent entry point (runtime.invoke_agent_for_user / prepare_agent_turn) for
web, email, iMessage, WhatsApp, scheduled and cron, so identity, permissions,
memories, summaries and metrics match across channels.
- Serialize turns per checkpoint thread; /query validates input and returns 409
while a durable run owns the conversation.
Reconnectable chat (durable runs)
- POST /runs creates an idempotent, owner-scoped run; GET .../events replays an
ordered SSE log by cursor, so generation survives refresh and device switches.
Stop cancels; disconnect only detaches. /stream-query is a compatibility adapter.
- Coalesce streamed deltas (150 ms) and wake same-process subscribers instead of
polling; keep buffered text when a run is stopped.
- Drain runs on shutdown; serve via Hypercorn with a shutdown timeout covering
the drain (and no longer in debug mode). CHAT_RUN_* settings are forwarded.
Scheduled messages and cron
- Atomic delivery claims, attempt history and explicit retries; ambiguous sends
become "uncertain" and are never resent automatically.
- Cron triggers need a run_id/Idempotency-Key; legacy jobs need an explicit owner.
- Cancelling never rewrites a started delivery; series stop via tombstones.
Legacy sent rows are not forked into new series. Malformed bodies return 400.
History, integrations and retrieval
- Message-content search, pinned conversations, JSON/Markdown export and a
memory editor.
- Integration status panel and YNAB data-freshness provenance.
- Date-filtered document/note search with stable citations and a source viewer.
Migration 20 is generated by aerich.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent tools call ensure_fresh(120s) but the background loop ran every
900s, so most finance questions waited on an inline YNAB fetch. Default
the loop to 90s (40 requests/hour, well under YNAB's 200/hour limit).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ynab_spending_report 'total' was a net figure: inflows (deposits into
budget accounts) subtracted from the spending total, which the model
repeated back as spending. The report now states total outflows and
inflows separately, drops net-negative groups from the per-group list,
and computes the monthly average from outflows.
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>
- 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>
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>