# CEM CRM Shadow Consumer

Phase 7 is a no-side-effect comparison consumer. It reads the durable Event
Journal through the Phase 6 Consumer SDK and writes deterministic CRM decision
records. It does not call Vtiger, MySQL, WebSocket, AMI, archive, or AI services.
The legacy listener remains authoritative for production CRM behavior. Its file now contains an optional, disabled-by-default observation hook; that additive hook does not change listener business behavior.

Related architecture: MPBX-004 and MPBX-006. Release: `cem-crm-shadow-v0.7`.

CRM Shadow writes decision evidence only. It performs no CRM, Vtiger, MySQL, WebSocket or other production side effect, and no production side-effecting CEM CRM consumer exists yet.

## Legacy listener observations

Read-only inspection of `ami/listener.php` found these current rules:

- Calls initialize from `Newchannel`, keyed by `Linkedid` with `Uniqueid`
  fallback, only when an external caller identity is found and the context is
  not outbound/internal. A DAHDI inbound edge case can initialize before the
  number is available.
- CRM extension eligibility comes from active Vtiger users with
  `phone_crm_extension`; optional configured inbound roles further restrict it.
- A unique eligible agent leg on `DialBegin` produces the legacy ring popup
  notification. Repeated ring notifications per call/extension are suppressed.
- The first eligible `Newstate Up` or `DialEnd ANSWER` resolves the answered
  extension and produces the answered notification. Later answers are ignored.
- An answered agent-channel hangup finalizes one completed inbound PBXManager
  activity. Unanswered calls wait before missed finalization and are discarded
  if no eligible CRM extension rang.
- Finalization is protected by per-call latches. Caller identity is extracted
  from several AMI number fields and used for contact lookup.
- The inspected active listener does not attach recording references in this
  path; Phase 7 recording candidacy is therefore an explicit shadow policy.

The canonical model does not reproduce every raw AMI timing detail. Phase 7
assumes queue, agent, direction, and recording facts are present in canonical
payload or controlled extensions. It never reads production configuration or
database-derived extension data.

## Configuration

Copy `config/crm_shadow_config.inc.php.sample` to a runtime-only local path. The
configuration supplies allowed directions, tenant/environment scope, popup and
activity lifecycle events, recording policy, CRM extensions, and monitored
queues. Do not place credentials in this file.

The extension registry records enabled state plus optional opaque CRM user,
team, or tenant metadata. The queue registry records enabled state, display
reference, permitted extensions, and optional tenant/environment scope. Both are
in-memory views of the local configuration; neither queries MySQL.

## Decision records

Each append-only JSONL record includes the requested journal/event/correlation
identities, canonical subject, scopes, direction, queue and agent facts, masked
caller identity, decision and safe reason, candidate flags, legacy rule
reference, source journal/offset, and checksum. `logical_action_key` is included
to make idempotency auditable. The canonical event and unrestricted caller data
are never embedded.

Decisions are `applicable`, `not_applicable`, `insufficient_context`,
`duplicate`, or `policy_blocked`. Common reasons include missing direction,
queue, agent, caller or recording context; unmonitored queues; unknown or
disabled extensions; scope mismatch; and duplicate logical action.

Logical keys are:

- `crm-popup:{correlation_id}:{agent_extension}`
- `crm-activity:{correlation_id}`
- `crm-recording:{correlation_id}:{recording-identity-hash}`

The writer scans existing decisions on startup, suppresses event replay, and
allows at most one applicable decision for each logical action. Files are mode
`0600`, append-only, flushed and synchronized per record. Torn decision tails
fail safe.

## CLI and dry-run

```sh
php tools/cem_crm_shadow.php \
  --journal /tmp/cem-journal/journal.jsonl \
  --config /tmp/cem-crm-shadow/config.php \
  --decisions /tmp/cem-crm-shadow/decisions.jsonl \
  --checkpoint /tmp/cem-crm-shadow/checkpoint.json \
  --once
```

`--dry-run` uses the SDK replay mode, prints only event/correlation identity,
decision, reason, and candidate booleans, and changes neither normal checkpoint
nor decision output. SDK filtering restricts handling to the five supported CEM
event types. Normal checkpointing, retry, dead-letter, follow, restart, partial
line, journal identity, and locking behavior are inherited from Phase 6.

## Legacy comparison

`--legacy-observations PATH` accepts sanitized JSONL observations containing a
correlation or legacy call reference, queue, extension, popup/activity booleans,
timestamp, and safe reason. It reports matched, missing, extra, queue mismatch,
extension mismatch, timing delta, and action mismatch counts. Differences never
modify policy automatically. Invalid observation lines are ignored without
printing their contents.

## Manual validation

Run the command above twice. The first pass should create one decision per
supported journal event. The second must report zero new decisions and resume at
the journal end. Then run with `--dry-run` and verify the decision and checkpoint
checksums and modification times remain unchanged.

## Limitations

CRM user/contact lookup, legacy delayed-missed timing, ring notifications to
multiple simultaneous agents, and persistent cross-restart call enrichment are
not reproduced. Aggregate context available directly on each canonical event is
preferred. Decision files are scanned linearly for restart idempotency and have
no retention or compaction policy in this phase. Local registries are reviewed shadow configuration, not production-equivalent CRM lookup. Multi-agent live parity remains incomplete, and this component is not a production CRM replacement.
