Después de validar el agente E2E con DeepSeek, el legacy queda muerto.
51 archivos cambiados (la mayoría borrados), el motor único es ahora el
agente tool-calling.
Borrados (~3500 LOC):
- src/modules/3-turn-engine/nlu/ (router + 4 specialists + promptLoader +
schemas + humanFallback + 6 default prompts) — reemplazado por systemPrompt.js
- src/modules/3-turn-engine/stateHandlers/ (cart.js, cartHelpers.js, idle.js,
shipping.js, utils.js, index.js) — reemplazado por tools del agente
- src/modules/3-turn-engine/stateHandlers.js (re-export shim)
- src/modules/3-turn-engine/openai.js (NLU clásico v3 + jsonCompletion +
llmRecommendWriter + llmPlanningRecommend) — el agente crea su propio
cliente OpenAI con tools nativos
- src/modules/3-turn-engine/replyRewriter.js (rewriting LLM) — el agente
escribe say directo, no necesita reescribir
- src/modules/3-turn-engine/replyTemplates.js + test (rotación de variantes)
— el agente varía naturalmente con tool_choice=required + temperature
- src/modules/3-turn-engine/recommendations.js (cross-sell + planning) —
el agente decide cuándo recomendar via tool calls
- src/modules/3-turn-engine/machine/ (XState v5 completo + 19 tests) —
reemplazado por la FSM podada en fsm.js + agent/runTurn.js
- src/modules/3-turn-engine/turnEngineV3.helpers.js, .units.js,
.pendingSelection.js (helpers del legacy)
- src/modules/0-ui/controllers/prompts.js, handlers/prompts.js,
db/promptsRepo.js — admin de prompts NLU (ya no hay prompts editables)
- public/components/prompts-crud.js + nav entry en ops-shell
turnEngineV3.js se reduce a un thin wrapper que exporta runTurnV3 (alias
de runTurnAgent) + safeNextState (re-export de fsm.js). Mantiene la firma
pública para no tocar pipeline.js.
Activado:
- AGENT_MAX_TOOL_CALLS=10 y AGENT_TURN_TIMEOUT_MS=25000 son los únicos
flags. Borradas: USE_MODULAR_NLU, USE_XSTATE, XSTATE_SHADOW,
XSTATE_SETTLE_MS, REPLY_REWRITER, REPLY_REWRITER_TIMEOUT_MS, TURN_ENGINE,
AGENT_TURN_ENGINE, AGENT_TURN_ENGINE_SHADOW (el agente es default).
Prompt caching DeepSeek:
- systemPrompt.js: era función con storeName interpolado → ahora export
const SYSTEM_PROMPT (100% estático). storeName se pasa por user message
via working_memory.store.name. Cualquier cambio al system invalida cache,
por eso es estático estricto.
- runTurn.js: captura usage.prompt_cache_hit_tokens (DeepSeek) o
prompt_tokens_details.cached_tokens (OpenAI compat) y suma a métricas.
- /api/metrics/agent ahora reporta prompt_tokens_total,
completion_tokens_total, prompt_cache_hit_tokens, cache_hit_ratio.
- Smoke test 3 turnos: cache_hit_ratio = 0.72 (17664 cached / 24546 total
prompt tokens). Saving directo en costo: ~$0.02/M cached vs $0.27/M no
cached en DeepSeek.
Tests: 148/148 (perdimos 90 tests del legacy XState/replyTemplates que
ya no aplican). Sim flow E2E confirmado: hola → agent responde, multi-turn
con cache caliente.
Si más adelante hace falta volver al legacy: git revert este commit
(c c9c69cf8 es el último estado verde con doble motor).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 bloques en una pasada:
A. Scroll fixes (síntoma reportado por el usuario):
- run-timeline: nuevo flag _userScrolledUp con detección por rAF en
bindScroll. Auto-scroll al final SOLO si el usuario está abajo
(umbral 150px) o si él mismo disparó un optimistic bubble. Cuando
el usuario manda mensaje, se resetea el flag y se scrollea.
- conversation-inspector: mismo patrón. ui:chatScroll respeta el
flag userScrolledUp del emisor para no perseguir al usuario que
lee arriba.
- .bubble: + min-width:0, overflow-wrap:anywhere para URLs/JSON
largos sin espacios.
- <pre>: + overflow-x:auto, max-width:100%.
- chat-simulator: textarea con resize:vertical, min/max heights
viewport-friendly. inputs-col con overflow-y:auto.
B. Stale state options:
- conversation-list y conversations-crud: dropdowns ahora muestran
IDLE / CART / SHIPPING / AWAITING_HUMAN. Quitados BROWSING,
BUILDING_ORDER, WAITING_ADDRESS, WAITING_PAYMENT, COMPLETED.
- main.js: simulated plan.next_state pasa a CART.
C. SSE resilience (lib/sse.js):
- connect() con backoff exponencial (1s → 30s).
- safeParse() helper: cada evento envuelve JSON.parse en try-catch
para que un payload malformado no rompa otros listeners.
- reset retryDelay al primer "hello" exitoso.
- ops-shell: indicador con dot (verde "En vivo" / naranja pulsante
"Reconectando…") en lugar de texto plano.
D. Toast service global (public/lib/toast.js):
- API simple: toast({ kind, text, ms }). Apila, animación slide-in,
auto-dismiss 4s. Click para cerrar.
- safeFetch en api.js: wrapper que dispara toast en network error y
non-OK. Migrados simEvolution + retryLast.
- chat-simulator usa toast en lugar de status text efímero.
E. Theming con CSS vars:
- public/styles/theme.css con paleta completa (panels, borders,
text, accents, bubbles, charts, radii, shadows). Linkeado desde
index.html.
- Migrados a var(--*) los 5 componentes más visibles:
run-timeline, chat-simulator, conversation-inspector,
conversation-list, home-dashboard. Custom properties heredan a
través del shadow DOM, así que los demás componentes pueden
migrar gradualmente sin cambios estructurales.
- home-dashboard ya tenía vars locales: ahora apuntan a las globales.
Backend: 192/192 tests pasando. Sin cambios de API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7 frentes en una pasada:
1. Idempotency en pipeline.processMessage: si el message_id ya existía
(Evolution suele reentregar webhooks), skip todo el turn y devuelve
{ duplicate: true }. Antes el ON CONFLICT DO NOTHING evitaba el insert
pero igual procesaba NLU + side effects.
2. Limpieza de payment residual de admin/UI:
- ordersRepo.getMonthlyStats / getTotals: out las queries cash/card
- home-dashboard: out el donut "Efectivo vs Tarjeta"
- orders-crud: out columna "Pago" + sección de detalle de pago
- conversation-inspector: out 💵💳✅ del resumen
- takeovers: out payment_link en run, out payment del summary
- public/main.js: out la invariant no_checkout_without_payment_link
- prompts-crud: out la entry "payment" del PROMPT_LABELS
- wooOrders.parseOrder: out lectura de payment_method/is_paid (estado
del pago lo gestiona el comercio offline, fuera del bot)
- ordersRepo: out is_cash/is_paid del row mapping
3. Seed migration 20260501130000_seed_piaf_settings_and_replies:
- schedule realista para piaf (delivery/pickup días+horas)
- delivery_zones con barrios CABA reales (Palermo, Belgrano, etc)
- 41 reply_templates con 17 keys + variantes (todo lo de DEFAULTS)
Permite editar respuestas sin redeploy y desbloquea {{store_hours_today}},
{{delivery_zones_summary}} reales en los templates.
4. Address validation en shipping: nuevo checkAddressInZone() en
storeContext.js. Cuando el usuario da dirección, se valida contra
zonas configuradas. Si está fuera, renderiza shipping.address_out_of_zone
con sugerencia de zonas alternativas. Sin zonas configuradas → accept-by-default.
5. Métricas rewriter: getRewriterMetrics() expuesto, contadores
ok/fallback/timeouts + avg_ms + fallback_rate. Endpoint nuevo
/api/metrics/rewriter.
6. Shadow XState → audit_log: el shadow mode pasa de console.log a
insertAuditLog con entity_type='xstate_shadow'. Permite review post-mortem.
7. Recommend portado a XState: nuevo recommendActor (fromPromise) wrappea
handleRecommend; sub-state cart.recommending invoca el actor; ingestRecommendResult
absorbe { plan, decision } en context. RECOMMEND event funciona desde idle
y desde cart con USE_XSTATE=1.
8. tenantId opcional en renderReply/loadReplyVariants — defaultea a
getTenantId() del módulo shared/tenant.js. Backward-compat: callers
pueden seguir pasando tenantId o omitirlo.
E2E tests nuevos en machine/e2e.test.js: golden flow pickup, golden flow
delivery con address-in-zone, snapshot rehydrate full flow, universal
cart-on-add desde shipping. 192/192 tests pasando (188 previos + 4 E2E).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>