Replace the deprecated copilotRuntimeNextJSAppRouterEndpoint wrapper
(which was already the v2 single-route handler underneath) with
createCopilotRuntimeHandler from @copilotkit/runtime/v2, built once at
module scope, and plug in a custom PostgresAgentRunner:
- run() proxies the agent's event stream untouched — MAF persists every
turn in chatthreadsnapshot, so the BFF records nothing
- connect() forwards to MAF's snapshot hydration (empty-messages run →
RUN_STARTED → MESSAGES_SNAPSHOT → RUN_FINISHED, no LLM call), making
the DB the only conversation store — correct across clears, restarts
and future instances
- stop() aborts via a process-local live-run map (same pattern as the
official sqlite-runner); isRunning() has no caller in the v2 SSE path
This deletes the whole workaround layer the in-memory runner forced:
the connect intercepts (URL + envelope forms) and Asistente's
client-side hydration run. The ask-box flow now awaits connectAgent
instead. Auth moves to a single Request rewrite (ws_token cookie →
Authorization Bearer) before the handler — the v2 path forwards
authorization + x-* headers to both run clones and runner.connect.
Stays in single-route mode: same envelope protocol the client already
auto-detected, no exposure to CopilotKit#4953, /threads* REST not
needed. Telemetry disabled via COPILOTKIT_TELEMETRY_DISABLED.
Verified full matrix in dev: fresh ask, reload replay via runner,
3 cleared convos + double navigation (only last survives), card renders
once and survives follow-up + reload, Inicio ask-box with history,
stale-tab persists only its new turn. 126 backend tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Investigated whether CopilotKit offers an official thread flush instead
of the BFF connect intercept. It does have one (POST /threads/clear ->
runner.clearThreads()), but only on the fetch-router entrypoint — the
single-route adapter we mount (copilotRuntimeNextJSAppRouterEndpoint)
accepts exactly agent/run|connect|stop, info, transcribe at 1.62, so
threads/* is unreachable from our deployment (verified: 400 Unsupported
method). A custom DB-backed AgentRunner is the clean end-state, but the
AgentRunner base class isn't exported at 1.62. Note left in clearThread;
revisit both on the next CopilotKit upgrade.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The connect intercept only matched POST …/connect, but CopilotKit talks
to the runtime in single-route mode: everything POSTs to
/api/copilotkit with the operation in the body envelope
({method:"agent/connect"}). So the in-memory runner kept replaying the
BFF process's full event history on every CopilotChat mount — every
conversation since server start, across clears — which is exactly the
stacked resurrected conversations seen in prod after navigating away
and back. Now both transports get the immediately-completed stream.
Verified with the reported repro: three conversations with clears
between, navigate away and back → only the third remains; reload
hydration unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of the "chunks" clear bug: conversation history is
server-owned, but MAF seeds a thread from whatever the client sends
when no snapshot exists. Any stale tab (open across a clear or from an
older session) still holds the old conversation in memory, and its next
question re-persisted the whole thing — clears appeared to peel off one
layer at a time.
The agent middleware now drops client-sent history for threads with no
stored snapshot, keeping only the last user turn (fresh conversations
and empty-messages hydration requests pass through untouched). Verified
live: simulated a cleared-thread stale tab, asked from it, and only the
new turn was persisted.
Also: clearThread surfaces DELETE failures as a toast instead of
silently reloading, and conftest sets a dummy OPENAI_API_KEY so tests
can import app.main.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MAF persists the thread at run END, so clearing mid-stream deleted the
row only for the finishing run to write the conversation right back —
the thread reappeared after the reload ("chunks" needing a second
clear). The button is now disabled while agent.isRunning.
(The chunked-clear reports on the prior build had a second cause fixed
in 0f34a64: clearing left the client's message copy alive and the next
question re-persisted it.)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Aggregate answers were bare numbers ("Tu saldo neto hoy es -X."). The
prompt now requires showing the composition unprompted: net worth gets
the assets/liabilities split plus per-account table, day spend gets the
purchases behind the total, cycle/category totals get their top
components — one headline sentence, a compact table, and a closing
observation only when the data supports one. Also bans filler offers
("si quieres te puedo mostrar…") in place of just showing it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Clearing in place (setMessages + remount) left the conversation
rendered — CopilotKit keeps chat state beyond agent.messages — so the
button now reloads the page after deleting the thread: a fresh boot
hydrates from the empty store and shows the welcome screen with
suggestions, which previously only appeared after a manual refresh.
Supporting changes:
- CopilotChat gets an explicit agentId="wealthysmart" (without it, it
resolves DEFAULT_AGENT_ID and can bind a different client agent
instance than useAgent's).
- The BFF answers CopilotChat's POST …/connect with an
immediately-completed stream: the runtime's in-memory runner replays
this PROCESS's event history, which resurrects cleared conversations
and double-feeds hydration. The empty-messages hydration run against
the DB store is the single restore path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 bug: earlier assistant replies vanished as new turns completed and
only came back on refresh. Debug capture showed why: with persistence
on, the run-end snapshot rebuilds history under STORE ids while the
client holds it under STREAMED ids (MAF re-mints post-tool-call text
ids, their #3619). ag-ui apply treats the snapshot as authoritative —
matching ids get replaced (streamed text wiped by the store's
empty-content toolCall entry) and the store's text copies were being
filtered out by ReconcileSnapshotMiddleware as presumed duplicates, so
prior answers had no surviving representation.
Reconciling id-mismatched snapshots is unwinnable; skip them instead.
During a normal run the client already built the turn from streamed
events and the snapshot adds nothing. Hydration replays (empty
input.messages) still pass through — there the snapshot IS the
conversation, and every reload converges the client to store ids.
ReconcileSnapshotMiddleware is replaced by the much simpler
DropRunMessagesSnapshotMiddleware.
Verified in dev: multi-turn history persists across runs, one card that
survives follow-ups, reload replays everything, no duplicates.
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>
After the client returns a render tool's result, the model sometimes
re-issues the identical call with a fresh id on the follow-up run,
painting a second copy of the spending-summary card (observed live on
MAF 1.10 / CK 1.62). DeduplicateToolCallMiddleware now tracks which
render tools already ran in the current user turn and drops repeats —
their streamed TOOL_CALL_* events and their entries in the run's
MESSAGES_SNAPSHOT. The system prompt also tells the model the card is
already displayed once its "ok" result is present.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Asistente.tsx pins threadId "main" on CopilotChat (an explicit stable
id stops the per-mount message wipe), runs a hydration pass on page
load — an empty-messages run makes MAF replay the stored snapshot as
MESSAGES_SNAPSHOT with no LLM call — and sends the Inicio ask-box
question only after hydration so every normal run carries the full
client-known history. "Nueva conversación" clears the stored thread
behind a ConfirmDialog.
- server.ts: the BFF no longer injects context into empty-messages
requests (that would defeat MAF's hydration trigger), and the snapshot
reconciliation passes hydration replays through untouched and treats
ids the client already sent as history rather than duplicates.
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>
Retiring it in the middleware audit was premature: MAF 1.10 reuses the
streamed id for plain-text snapshots but deliberately mints a fresh UUID
for post-tool-call assistant text (their #3619), so tool+text turns
still rendered a duplicate assistant bubble (re-verified live). The
user-message duplication that motivated the retirement only fired on
a2ui retry runs, which no longer exist now that a2ui is disabled.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CK 1.60+ validates generated a2ui ops against a component catalog before
painting, and our agent's ops fail validation — surfaces hang at
"Building interface" forever (MAF also drops RunAgentInput.context, so
the catalog/guidelines never even reach the model). Disable a2ui end to
end until the pipeline matures:
- server.ts: a2ui { enabled: false }; RENDER_TOOLS no longer includes
render_a2ui
- App.tsx: remove the a2ui provider prop
- agent.py: prompt now asks for clean markdown tables for lists and
structured data; render_spending_summary card guidance unchanged
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ReconcileSnapshotMiddleware removed: MAF now reuses the streamed
message id in MESSAGES_SNAPSHOT (verified in _agent_run.py at
python-1.10.0 and live event logs); keeping the surgery duplicated
user messages on a2ui follow-up runs.
- beforeRequestMiddleware now injects RunAgentInput.context as a system
message: MAF declares the field but never feeds it to the model,
which starves the model of CopilotKit's A2UI catalog/guidelines.
Remove when MAF consumes context upstream.
- Workaround chain env-gated: CK_MIDDLEWARES=off runs bare for audits,
CK_DEBUG_EVENTS=1 enables the event logger.
- Kept: SuppressRenderToolText, StripModelArtifacts,
DeduplicateToolCall, pairOrphanToolCalls (behavior-verified).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
No API drift: v2 hooks, v1 useCopilotAction interop, runtime endpoint
and a2ui config all verified against the v1.62.2 source. Brings the
1.60.2 chat scroll stability fixes and 1.57.2 thread-state fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Agent/OpenAIChatCompletionClient/add_agent_framework_fastapi_endpoint
APIs verified unchanged against the python-1.10.0 source; 115 tests
green, pip check clean. MAF 1.6.0 turns OTel on by default —
ENABLE_INSTRUMENTATION=false in both compose files keeps prod logs
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shell revamp left SidebarInset unbounded (wrapper is min-h-svh), so
Asistente's flex chain resolved to content height — the chat's internal
scroller never engaged and long answers hid behind the composer.
Asistente now gets h-svh + overflow-hidden on the inset; every other
page keeps body scroll. Verified: inset == viewport on /asistente,
body scroll intact elsewhere.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Unifies the transform pipeline on Oxc now that Rolldown does the
bundling. Dev server boot 353ms -> 188ms; build parity (2.03s).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The last JS-based stage of the toolchain (dev was already SWC+esbuild).
Local build: 18.75s/1.83GB peak RSS -> 1.97s/1.27GB, output verified
identical in the browser.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The UI revamp pushed rollup's peak past the 1536MB cap and the prod
image build OOM'd. Verified locally: fails at 1536, builds at 2048.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The default pointed at the compose-internal 'backend' hostname, which
never resolves when the Hono runtime runs on the host — every local
agent run died with fetch failed/ENOTFOUND before reaching FastAPI.
Prod is unaffected (sets both URLs explicitly).
Co-Authored-By: Claude Opus 4.8 (1M context) <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>
Gasto del ciclo reveals the by-source breakdown, Próximas cuotas the
full active-plan list, Pensión last-period rendimientos per fund.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Header search trigger + global hotkey. Typing a question surfaces a
'Preguntar' item that lands in Asistente via the same auto-send flow
as the Inicio ask box. Also installs hover-card and calendar for the
remaining Phase 3 work.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Financiamientos shows cuota progress as Progress bars; Sincronización
uses a single Card of Item rows; Items Recurrentes table wrapped in a
Card like every other table; both window.confirm sites replaced with
the app's ConfirmDialog.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PageHeader on Budget, Proyecciones, Pensiones, Planificador,
Municipalidad, Sincronización, Financiamientos, Asistente. Pensiones
fund identity and Municipalidad meter/charge series now use the
validated chart tokens (color-mix for translucent fills). Planificador
stat cards on StatTile; deterministic chart rendering everywhere.
Municipalidad page title now matches its nav/breadcrumb label.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lib/charts.ts centralizes the color system: fixed categorical slots
(never cycled, >5 folds into Otros, 'Sin categoría' reads neutral) and
color-mix ramps that stay correct in dark mode. Animations off so
charts render deterministically.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Categories fold into 'Otros' past 5 slots (fixed hue order, never
cycled); daily spending excludes future-dated Tasa Cero cuotas; rate
chart gets a legend; PageHeader with the cycle selector as action.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- PageHeader + StatTile standardize the four header and three stat-tile
variants found in the audit; Inicio and Salarios migrated as proof.
- chart tokens replaced with a teal-anchored categorical scale, CVD-
ordered and validated (six checks, light + dark surfaces).
- Registry adds: empty, field, item, spinner, progress, toggle-group,
input-group, popover, kbd (+ toggle dep).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Próximas cuotas: headline is the upcoming billing batch (sum of every
active plan's next cuota, using the rounding-absorbing last cuota
when it's the pending one) — the in-cycle formula was ~always zero
since cuotas bill on the 19th, day one of the next cycle.
- useAgent must name the 'wealthysmart' agent (default crashed the page).
- /transactions/recent excludes future-dated Tasa Cero cuotas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stat cards (gasto del ciclo, próximas cuotas Tasa Cero, pensión),
ask-the-assistant box that lands in Asistente and auto-sends via
agent.addMessage + runAgent, and últimas transacciones. Index route,
login redirects updated; Asistente stays at /asistente.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AppSidebar (icon-collapsible, sync warning badge, brand header),
NavUser footer (avatar + privacy/theme/logout dropdown), and a thin
header with SidebarTrigger, breadcrumbs, and quick toggles. Mobile
drawer and cmd+B come from the sidebar primitives.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
navigation.ts becomes the single source of truth for sidebar and
breadcrumbs (Inicio entry added; '/' is exact-match).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>