Backend:
- 3 migrations: system_users (citext email único, password_hash, active),
system_sessions (UUID + expires_at + revoked_at), ALTER audit_log con
actor_user_id/actor_email/actor_ip/action_path/summary y entity_id NULL.
- src/modules/auth/: usersRepo, sessionsRepo, passwords (bcrypt cost 10),
auth (login/logout), bootstrap (crea admin desde ADMIN_EMAIL/PASSWORD si
la tabla está vacía). 4 tests passwords (hash distinto cada vez, verify
rechaza, longitud mínima 8).
- middleware/requireAuth: lee cookie bot_session, busca sesión activa,
popula req.user. Whitelist: /styles, /components, /lib, /login, /, /home
y SPA paths (HTML carga sin auth, el JS gatea con /api/auth/me).
- middleware/auditWriter: registra cada POST/PUT/DELETE 2xx en audit_log
con req.user, IP, body redactado (passwords/tokens/secrets). Handlers
pueden enriquecer summary via res.locals.audit.
- routes: /api/auth/{login,logout,me} (cookie httpOnly + DB session),
/api/system-users (ABM con guards: cant_delete_self, cant_deactivate_self,
email único, password ≥ 8), /api/audit-log + /api/audit-log/actors.
- src/app.js: orden estricto — webhooks (sin auth) → auth routes (sin auth)
→ /login HTML → static → SPA HTML → requireAuth + auditWriter → API admin.
Bootstrap del primer admin se ejecuta en index.js antes de listen. Usa
ADMIN_EMAIL/ADMIN_PASSWORD/ADMIN_NAME del .env. Si no están seteados y la
tabla está vacía, warn y exit (nadie puede loguearse).
Frontend:
- /login.html + /login.js: form simple, POST a /api/auth/login con
credentials:include, redirect a ?next=... o /home. Si ya hay sesión
activa, va directo a /home.
- public/app.js gate: chequea /api/auth/me antes de initRouter; sin sesión
redirige a /login?next=<path>. window.__USER__ disponible para shell.
- ops-shell: nav agrega "Operadores" + "Actividad". Header derecha muestra
email del user + botón Salir (POST /api/auth/logout + redirect /login).
- system-users-crud: CRUD lista/form (estilo settings). Crear/editar/
cambiar password/eliminar. UI muestra badge "Vos" + bloquea eliminarse
ni desactivarse a uno mismo.
- audit-log: tabla read-only con filtros (actor, entity_type, since,
search), paginación 50, badges por acción, modal de detalles con
changes JSON. /api/audit-log/actors pobla el dropdown de operadores.
Smoke E2E: login OK + cookie set, /me 200; logout → /me 401; settings POST
genera fila en audit_log con actor_email + action_path; ABM crea/borra
operadores con guards; intentar borrarse devuelve 400 cant_delete_self.
161/161 tests verde (pre-existentes 157 + 4 passwords nuevos).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Persistencia: cuando confirm_order encola create_order y la orden tiene
shipping/zona/window, runTurn snapshot a context.last_delivery
{is_delivery, shipping_address, matched_zone, pending_location, delivery_window}.
pipeline preserva ese campo cuando resetea por stale (>24h), igual que
external_customer_id.
Agente lo ve via working_memory.last_delivery en cada turno.
Nueva tool reuse_last_delivery() que copia shipping_address + matched_zone
(+ pending_location) al order actual. Pickup-only sólo setea is_delivery=false.
systemPrompt: instrucciones para que el bot proactivamente ofrezca "te lo
mandamos al mismo lugar que la última vez (dirección, zona, $)" cuando
last_delivery existe y todavía no se eligió método de envío. Cliente puede
aceptar (reuse_last_delivery) o pedir otra dirección/retiro. delivery_window
NO se asume — siempre se vuelve a preguntar día/hora.
Smoke E2E: cliente recurrente con conversación stale 25h+
- 1ra orden: 1kg vacío → location → mar 12h → confirma.
- DB: context.last_delivery con zona Centro Test + dirección + ventana.
- 2da orden: "hola, 500g bondiola" → bot: "¿al mismo lugar (Av. Corrientes
1234, Centro, $1.500)?" → "sí" → "¿qué día? La última fue mar 12h, puede
ser otro" → "jueves 11hs" → orden cerrada sin re-pedir pin.
Header: "Bot Ops Console" → "Piaf Console" (index.html + ops-shell).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Borrado public/components/test-panel.js + su nav button + su rango de
router + sus rutas /test/products-with-stock y /test/order.
- Borrados handleGetProductsWithStock + handleCreateTestOrder y los
api wrappers getProductsWithStock + createTestOrder. Ya no se usan en
ningún flujo: pruebas de bot se hacen vía simulator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>