The CR-hardcoded today broke two scenarios: the owner traveling, and
any future non-CR user. Now the provider sends the browser's IANA
timezone (X-Client-Timezone) with every CopilotKit request — the
runtime forwards x-* headers to the agent on its own — and the backend
binds it to a per-request ContextVar next to the DB session.
get_current_date and the future-cuota bounds use it; the tool also
reports the timezone so the model can echo it. Unknown or absent zones
(n8n posts, tests) fall back to Costa Rica; comma-joined duplicate
header values are tolerated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prod showed the failure mode after the date fix: get_current_date
returned the right day, but the model answered "¿cuánto he gastado
hoy?" from cycle-level aggregates (the only spend tools it had) and
concluded ₡0. Give it a day-grained tool — per-day CRC totals mirroring
/analytics/daily-spending (COMPRA only, no installment anchors, no
future cuotas) — and a prompt rule to match tool to time grain.
Verified: fresh thread now calls get_current_date →
get_daily_spending(2026-07-04, 2026-07-05) and answers correctly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two bugs from prod (server clock is UTC, user is UTC-6):
- get_recent_transactions returned future-dated Tasa Cero cuotas; it now
excludes anything dated after the user's today, same rule as
/transactions/recent.
- "¿Cuánto he gastado hoy?" answered for the wrong day: the prompt baked
date.today() in at boot — frozen from startup AND in server-UTC (8 pm
in Costa Rica is already tomorrow in UTC). The prompt no longer carries
a date; a new get_current_date tool returns today in CR time plus the
active billing cycle, and the prompt directs the model to call it for
any relative date reference. today_cr() lives in timeutil (CR has no
DST, fixed UTC-6).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Back MAF's opt-in AG-UI snapshot persistence with a DB store so the
conversation survives page reloads and restarts:
- ChatThreadSnapshot: one upserted row per (scope, thread_id), JSONB
messages/state/interrupt (migration 794baf50635b)
- PostgresSnapshotStore implements the AGUIThreadSnapshotStore protocol.
It opens its own sessions (MAF saves during SSE streaming, after the
request middleware has closed the ContextVar session) and sanitizes on
save: camelCase toolCalls/toolCallId (the @ag-ui/client Zod schema
strips snake_case, which would lose cards on replay), drop BFF ctx-*
context messages, dedupe MAF's re-recorded multi-run turns, and drop
orphaned tool results
- Endpoint mount gains snapshot_store + a constant scope resolver
(single-user app; auth already enforced by agent_auth_and_session)
- DELETE /api/v1/chat/thread resets the conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
by-category and daily-spending accept start_date/end_date (range wins
over cycle); Analytics gets a two-month calendar range picker beside
the cycle selector. Fixes a latent float/Decimal TypeError that 500'd
spending_by_category whenever it had data — the category donut had
never rendered. Regression + range tests added.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Paste-import now returns each skipped duplicate WITH the matched
existing row (id, merchant, date, amount, source) instead of a bare
count, and accepts allow_duplicates to force-import false positives —
the practical fix for same-day identical purchases hashing together
(UX-20; supersedes the BE-08 hash change, which would have orphaned
every existing hash). POST /transactions/bulk applies
delete/set_category/set_deferred to up to 500 ids atomically. 82 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The n8n flows fail invisibly from the app's perspective (review UX-17,
the top trust gap). /api/v1/sync-status derives last-received + cadence
thresholds for BAC, salary, municipal, pensions and exchange rate from
existing data — no n8n integration needed. New Sincronización page
shows per-source freshness and a hint to check n8n; the sidebar item
gets an amber dot when any source is stale. Verified live against the
dev DB (correctly flags its stale BAC data at 43 days).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Exchange-rate fetchers log every source failure instead of silently
passing (BE-18); the rate tool exposes fetched_at so staleness is
visible (BE-20). Agent: unbound-session failures raise a clear
RuntimeError (BE-12); net worth converts via get_crc_multipliers with
last-known fallbacks instead of a hardcoded 600 CRC / 1.08 EUR guess,
and reports accounts it cannot convert rather than inventing numbers
(BE-24); budget tool enforces MIN/MAX_YEAR (BE-16); municipal receipts
gain offset paging (BE-17); category analytics prefetches names; the
module docstring pins the read-only tool policy (SEC-09). Note: the
refresh loop never swallowed CancelledError (BaseException since 3.8) —
ARCH-08 was a false positive.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
read_pdf_upload() bounds reads at 10 MB and requires the %PDF magic
before anything reaches pdftotext (SEC-06); rejections land in the
per-file errors list with the filename. Parsing now runs via
asyncio.to_thread so a slow PDF no longer blocks the event loop for up
to 30s (BE-14). Review notes: temp files were already context-managed
(BE-09 overstated) and the pension batch already commits atomically
(BE-10 overstated). 69 tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- FK rules via migration 7884505b16b3 (verified on dev with a rolled-
back probe): transactions/recurring items SET NULL on category delete
(this also fixes the 500 that DELETE /categories raised for in-use
categories), water readings CASCADE with their receipt. Models declare
ondelete to stay in sync. (ARCH-09, BE-11)
- compute_actuals_by_source: 3 grouped queries replace 10 single-
aggregate round-trips; behavior pinned by the existing cycle suite.
(BE-21)
- compute_cc_by_category prefetches category names (ARCH-15); agent
pension latest-per-fund resolved in SQL (BE-04); municipal water
consumption batched into one grouped query (BE-05).
- Deterministic pagination: date DESC, id DESC on all transaction
listings. (ARCH-12)
- New agent-tool tests (session binding, JSON-safe output, batched
queries): 64 tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Models use Money = Annotated[Decimal, PlainSerializer(float)] so storage
is exact NUMERIC(15,2) (rates 15,6) while JSON keeps emitting plain
numbers — the frontend contract is unchanged, pinned by
test_models_serialization.py. Service layers coerce to float at their
boundaries (projection math, rate multipliers, agent tool output, the
savings constants become Decimal) so no naive Decimal/float mixing can
raise. Migration 7f567c8bafdb applied to dev: per-row values and the
table sum byte-identical before/after; endpoints verified live on the
new schema. 59/59 tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers: billing-cycle characterization (the BE-06 convention, boundary
instants, year wraps, deferred transactions), projection engine
(recurring schedules, no-double-count, cumulative balances, overrides,
fresh start), auth (JWT roundtrip/expiry, API tokens, constant-time
creds, rate-limit window), fail-fast settings, and the BAC paste
parser. SQLite in-memory fixtures, exchange rates pinned — no network.
Also doubles as the float-behavior baseline for the Phase 2 Decimal
migration (plan 1.5). config.py moves to SettingsConfigDict to clear
the pydantic deprecation warning from test output.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>