tx-signals-temporal

TX — Signals + Temporal (transversales)

Categoría: Plumería (no contiene lógica de negocio) Estado: Propio sano (mantenido del target V2 documentado) Tamaño esperado: ~2-3k LOC


1. Responsabilidad

Dos transversales que envuelven todas las capas de V3 dándoles durabilidad, observabilidad, idempotencia y propagación de eventos. No deciden, no calcifican, no traducen — solo proveen el sustrato durable y el bus de eventos.

Transversal Función
TX.Signals Bus inmutable tipado que acumula eventos de las capas y los proyecta a superficies (tags ERP, comments, audit log, alerts internas).
TX.Temporal Durabilidad, retries por dominio de fallo, idempotencia por workflow_id, aislamiento de entornos.

2. NO es su responsabilidad

  • No conoce semántica fiscal ni contable. Pasa estructuras opacas (StandaloneIntent, ExecutionPlan, signals tipados) entre activities sin inspeccionarlas.
  • No agrega validaciones. Las validaciones viven en sus capas (M1, I1).
  • No mantiene estado per-tenant en memoria. Cada workflow es self-contained.
  • No es la única forma de ejecutar el pipeline. Tests y evals invocan el pipeline directo, saltándose Temporal. Diseño deliberado.

3. TX.Signals — bus de eventos

Filosofía

Las capas emiten eventos tipados sin saber quién los consumirá. El bus dedupea, valida tipado, y proyecta a las superficies correspondientes al final del pipeline (no en runtime).

"Sistema nervioso del pipeline; las fases emiten signals como side-channel observacional y la transversal los proyecta al final una sola vez."

Catálogo central

SIGNAL_REGISTRY central con todos los signals válidos. Cada uno tiene SignalSpec:

SignalSpec {
    code: str,                          # "DUDA_CUENTA", "ERROR_LINEAS_VS_TOTAL", ...
    severity: ERROR | WARNING | INFO,
    intent: REVIEW | INFORM | BLOCK,
    audience: list[INTERNAL | CLIENT_VIA_ERP_TAG | CLIENT_VIA_ERP_COMMENT | AUDIT_LOG],
    template_message_es: str,           # i18n string template
    blocks_execution: bool,
    dedupe_key_template: str?,
}

Cuando una capa emite un signal con código no registrado en SIGNAL_REGISTRY → falla. Tipado runtime.

Tipos de signals (categorías)

Categoría Ejemplos Audiencia típica
Decisión IA con duda DUDA_CUENTA, DUDA_IMPUTACION, DUDA_DEDUCIBILIDAD tag ERP + comment + audit
Decisión IA con fallback AVISO_CUENTA_GENERICA, AVISO_IMPUTACION_GENERICA tag ERP + audit
Error material ERROR_LINEAS_VS_TOTAL, ERROR_CUENTA_INVALIDA, ERROR_FECHA tag ERP + comment + alert
Provider info PROVEEDOR_NUEVO, PROVEEDOR_PRIMERA_VEZ_EN_TRIMESTRE tag ERP + audit
Calcificación REGLA_APLICADA, REGLA_PROPUESTA_PROMOVIDA, REGLA_INVALIDADA audit + alert ops
Reverse-sync EDITED_BY_HUMAN, CRITERION_CHANGED, APPROVED_BY_HUMAN audit + (a veces) alert ops
Validación post-write READBACK_DRIFT, COMPENSATION_REQUIRED audit + alert

Dedupe

Cada signal tiene dedupe_key (template parametrizado por document_id, plan_id, etc.). El bus mantiene seen_dedupe_keys por workflow execution. Si un signal con la misma key se emite dos veces → segundo emit se descarta.

Esto previene duplicación cuando una capa emite el mismo signal varias veces durante un workflow.

Proyección a surfaces

Al final del workflow (después del Executor, antes de cerrar el WF), un componente signal_projector consume el SignalBag y proyecta:

Surface Cómo
Tags ERP holded.add_tags(document, [signal.code para signal con CLIENT_VIA_ERP_TAG audience])
Comments ERP holded.post_comment(document, render_signals_as_md([signal con CLIENT_VIA_ERP_COMMENT])) con dedupe por hash global
Audit log Intelia INSERT en signal_audit_log table, todos los signals del WF
Alerts internas Slack/PostHog para signals con severity=ERROR o cuando un patrón excede umbral

La projection es idempotente (al re-ejecutarse, no duplica tags ni comments). Esto es importante porque la lógica de signal lives in different layers — solo se materializa una vez al final.

Frontera con T1 (signals existente del target V2)

Esta sección hereda el diseño de transversal-signals.md del target V2. Cambios menores:

  • Adapter explícito para convertir ValidationFinding (Fase 5) y FinalValidationFinding (Fase 9) a signals via tabla declarativa (validation_code → SignalSpec).
  • Audience filter enforced: signal con audience=INTERNAL NO sale a Holded.
  • Mensajes humanos i18n: castellano en messages_es.py, no en if/elif del código.

4. TX.Temporal — durabilidad

Filosofía

El pipeline corre como parent workflow + child workflows durables. Cada bloque racional de side effect (LLM call, group provider idempotente, DB read/write) es un activity. Crashes a mid-pipeline reanudan desde último boundary exitoso.

"Es plumería, no lógica de negocio. La pipeline real es invocable desde tests, evals y los activities — todos por igual."

Estructura

                    AccountingV3OrchestrationWorkflow (parent)
                                  │
                ┌─────────────────┴────────────────┐
                │                                  │
        MotorWorkflow (child)            IntegratorWorkflow (child)
        (M1: fases 1-5)                  (I1: plan gen + executor + final val)
                │                                  │
        ┌───────┴───────┐                  ┌───────┴───────┐
        │ activities    │                  │ activities    │
        │ - prepare_    │                  │ - lookup_plan_│
        │   facts       │                  │   store       │
        │ - decide_     │                  │ - call_plan_  │
        │   account     │                  │   generator   │
        │ - decide_cost_│                  │ - execute_    │
        │   center      │                  │   provider_   │
        │ - decide_     │                  │   group       │
        │   fiscal      │                  │ - capture_    │
        │ - compose_    │                  │   readback    │
        │   plan        │                  │ - validate_   │
        │ - validate_   │                  │   final       │
        │   invariants  │                  └───────────────┘
        └───────────────┘
                                                  │
                                                  ▼
                                  ┌────────────────────────────┐
                                  │  HighConfidence?           │
                                  │     ├─ Yes → executor      │
                                  │     └─ No  → dashboard wait │
                                  └────────────────────────────┘

Para reverse-sync (T4), hay un workflow distinto que corre asíncrono:

ReverseSyncMonitorWorkflow (independiente, continuous)
        │
        ├── poll_holded_changes_activity
        ├── interpret_comments_activity (LLM)
        └── emit_signals_to_bus_activity

Para calcificación (T3), workflow más complejo:

CalcificationLoopWorkflow (independiente, scheduled)
        │
        ├── collect_evidence_activity
        ├── propose_rule_activity (genera candidates)
        ├── shadow_test_activity (compara contra agente N=15 ejecuciones)
        ├── wait_for_intelia_ops_approval (signal-based)
        └── promote_rule_activity

Retries por dominio de fallo

Dominio Política Reasoning
LLM call 3 reintentos, backoff exponencial 1s → 10s LLM falla por rate limit o glitch transitorio
Provider API write 5-10 reintentos con backoff Holded a veces 5xx; recovery alto
DB read 10 reintentos rápidos Casi siempre conexión transitoria
DB write con constraint 1 intento, no retry Constraint failure = bug; no se arregla solo

non_retryable=True para errores de input validation, schema mismatch, etc.

Idempotencia por workflow_id

workflow_id determinista por document_id + version. Reuse policy REJECT_DUPLICATE en prod. Disparar el mismo documento dos veces no duplica trabajo.

Para reprocessing (e.g. re-run con nueva versión de M1): workflow_id = f"doc-{document_id}-v{m1_version}". Nuevo workflow_id permite re-run sin colisión.

Aislamiento de entornos

Workers arrancan con dos namespaces:

  • intelia-prod (o intelia-stage): para workflows productivos.
  • evals: aislado, para batch evals sin contaminar history productivo.

Tests locales saltan Temporal completamente.

5. Frontera entre M1 / I1 / T3 / T4 / S1 y TX

TX es plumería pura. NO contiene lógica fiscal, contable, ni de integración. Solo:

  • Encadena activities.
  • Reintenta según política.
  • Dedupea signals.
  • Proyecta signals al final.
  • Aísla failure domains.

Si una decisión semántica entra en TX (e.g. "si el plan tiene confidence baja, skip el executor"), está en el sitio equivocado. Esa decisión vive en I1 o en S1, no en el orchestration.

6. Riesgos arquitectónicos y mitigaciones

Riesgo Mitigación
Signals se quedan in-memory y no se proyectan si el workflow crashea Signal bag persiste en Temporal state como parte del workflow → recoverable on resume
Latencia adicional por mucha activity granular Activities con failure domain compartido se agrupan; no 1 activity por call HTTP individual
workflow_id colisión cuando reprocessing manual version suffix explícito en re-runs; Intelia ops controla
Dedupe key de comments rompe cuando cambia COMPOSER_VERSION dedupe_key incluye composer_version para forzar repost cuando se actualiza copy
Activities con LLM consumen mucha cola y bloquean otros Worker pool tuneado por tipo de activity. LLM activities tienen pool dedicado
Reverse-sync monitor workflow corre forever pero consume memoria/recursos Continuous workflow con continue_as_new periódico para liberar history

7. Lo que puede salir mal

Patología 1: cada capa empieza a emitir signals indiscriminadamente. A los 12 meses hay 200+ tipos de signals, la projection es ruido, el contable ve 20 tags por documento. Mitigación: catálogo central SIGNAL_REGISTRY con review obligatorio para añadir nuevo signal. Lint que prohíbe emit_signal("custom_code", ...) con código fuera del registry.

Patología 2: dedupe de comments rota. Comments duplicados en Holded porque el dedupe_key no incluye un campo. Mitigación: hash de signal bag completo, no per-signal. Si el bag cambia → repost; si no → skip.

Patología 3: workflow_id no determinista cuando hay reprocessing concurrente. Dos jobs intentan re-procesar el mismo documento → race. Mitigación: workflow_id con version explícito + check de "ya en progress" antes de start_workflow.

Patología 4: Temporal history se infla con signals muchos. Cada signal añade evento al history. 1000 signals por workflow → history pesa MB. Mitigación: signals con audience=INTERNAL se persisten fuera del workflow (a DB Intelia). El workflow solo carga referencias.

8. Open questions

  1. Catálogo Signal Registry: tipos exactos. ¿Cuántos signals? Propuesta: ~40 al arranque, expandir según necesidad con review obligatorio.

  2. Versioning del SIGNAL_REGISTRY. Cuando se renombra un signal, ¿qué pasa con audit logs históricos que tienen el código viejo? Política de aliasing.

  3. Granularidad de activities. ¿call_plan_generator es una activity (con todas las tool calls del LLM dentro) o son varias activities (una por tool call)? Trade-off granularidad vs latencia.

  4. continue_as_new en monitor workflows. ¿Cada cuánto? Si muy frecuente, sobre-coste; si poco, history grande. Propuesta: cada 1000 events.

  5. ¿Activities asíncronas tras workflow completion? Como projection a alert canal lento. Permite que el workflow cierre rápido y la projection sucede async.

  6. Aislamiento de entornos para multi-tenancy. ¿Cada cliente tiene su namespace? ¿O todos en el mismo? Decisión que afecta seguridad y operacional.

9. Métricas operacionales

Métrica Target
tx_workflow_latency_p95 <60s para path feliz, <120s para con LLM Plan Generator
tx_workflow_failure_rate <0.5% (excluyendo non-retryable errors expected)
tx_signal_projection_success_rate >99% (signals proyectan a tags/comments/audit)
tx_temporal_history_size_avg <500 events por workflow (sino, dividir en sub-workflows)
tx_reverse_sync_workflow_lag <30s desde edit del contable a signal emitido
tx_dedupe_save_rate Mide signals deduplicados — útil para detectar bugs de over-emission

Diferencias respecto al target V2 documentado

El target V2 ya tiene transversales Signals + Temporal bien diseñadas (transversal-signals.md, transversal-pipeline-temporal.md). V3 las conserva mayoritariamente, con cambios menores:

  1. Adapter explícito Validation → Signals. En V3 hay menos validaciones (filtro de 3 condiciones), pero las que sobreviven generan signals vía tabla declarativa.

  2. Workflows nuevos para T3 y T4. El target V2 no los tenía porque T3 y T4 no estaban diseñadas.

  3. Signal Registry como contrato versionado (no solo dict). Cualquier signal nuevo pasa por PR con review.

  4. Persistencia de signals INTERNAL fuera del workflow history para mantener Temporal history compact.

El resto del diseño (parent + 2 children, durabilidad, retries por dominio, idempotencia por workflow_id, aislamiento de entornos, pipeline invocable sin Temporal) se mantiene.


Fin de los docs Claude

Componentes documentados:

  • v3-architecture.md — alto nivel
  • m1-motor.md
  • i1-integrator-holded.md
  • t3-calcification.md
  • t4-reverse-sync.md
  • s1-ux-hybrid.md
  • tx-signals-temporal.md

Esperando versión paralela de Codex en ../v3-conceptual-codex/ para síntesis final.