---
module: agent.activeteller
title: Active Teller (IAT) — teller-session, core-banking & Check21 monitoring for Nautilus Hyosung ITMs
tab: Architect
order: 30
audience: dev
---

> **Internal · architecture.** Written 2026-07-01 from `hiveops-module-activeteller` source. Verify against code before relying on specifics.

## Module shape

Package `com.hiveops.activeteller`, six classes, no controllers/DB/Kafka (this is an agent module, not a microservice — see [technical.agent]):

- **`ActiveTellerModule`** — the `AgentModule` SPI entry point (registered via `META-INF/services/com.hiveops.core.module.AgentModule`). Owns lifecycle (`initialize()` → `isEnabled()` → `start()` → `stop()`), the shared single-thread `ScheduledExecutorService` (daemon thread named `activeteller`), and constructs the three monitors + the emitter.
- **`TellerSessionMonitor`** (package-private) — parses `ActiveTellerAgent_*.log`.
- **`SymXchangeMonitor`** (package-private) — parses `SymXchange{date}.log`.
- **`CheckProcessingMonitor`** (package-private) — parses `CheckProcessingResults{date}.log`.
- **`RollingLogTailer`** (package-private) — shared tail-by-offset reader; each monitor owns one instance.
- **`IatEventEmitter`** (package-private) — thin wrapper over `IncidentEventClient` that builds the `CreateJournalEventRequest` and centralizes send-error handling.

The three monitors are plain constructed objects, not their own `AgentModule`s — `ActiveTellerModule` drives all three from a single poll loop.

## Lifecycle & config gating

`initialize(ModuleContext)` is defensive and never throws for a mis-provisioned box — it just leaves `ready=false`, which `isEnabled()` returns. It bails (module stays disabled) when any of these fail, each with a distinct log line:

1. `device.type != NH_IAT`
2. `activeteller.log.dir` unset
3. `activeteller.log.dir` is not an existing directory
4. neither `agent.endpoint` nor `incident.endpoint` is configured

Endpoint prefix is chosen the standard way: `prefix = agentEndpoint != null ? "agent" : "incident"`, then `HttpClientSettings settings = HttpClientHelper.loadSettingsFromProperties(props, prefix)`. The `IncidentEventClient` is built once with `(settings, atmName, country)` from `ModuleContext`. `isHealthy()` = scheduler alive and not terminated.

## Flow: poll → tail → detect → emit

```
ActiveTellerModule.start()
  → ScheduledExecutorService (daemon "activeteller")
    ├─ scheduleAtFixedRate(poll,      0,            pollSec,      SECONDS)   // default 30s
    └─ scheduleAtFixedRate(heartbeat, heartbeatSec, heartbeatSec, SECONDS)  // default 900s
         poll():
           tellerMonitor.poll();  symXchangeMonitor.poll();  checkMonitor.poll();
         heartbeat():
           if any monitor isAlive() → emit IAT_HEARTBEAT with concatenated fragments
```

`poll()` wraps all three in a try/catch so one monitor's parse error can't kill the scheduler. `heartbeat()` skips entirely if no monitor has seen a line today (`isAlive()` = `tailer.currentFile() != null`), so idle machines stay quiet.

### `RollingLogTailer` — the shared tail engine

Every monitor reads through one `RollingLogTailer` constructed with `(baseLogDir, tag, FileResolver)`. On each `poll(Consumer<String>)`:

- Computes `dailyFolder = baseLogDir.resolve(yyyyMMdd)`; if it isn't a directory, no-ops.
- Calls the `FileResolver` to pick the concrete file inside that folder:
  - `fixedName(prefix, suffix)` → `{folder}/{prefix}{yyyyMMdd}{suffix}` (SymXchange, CheckProcessingResults).
  - `newestMatching(prefix, suffix)` → the most-recently-modified file matching prefix+suffix (session-stamped `ActiveTellerAgent_*.log`).
- Tail-by-offset: opens with `RandomAccessFile`, `seek(fileOffset)`, reads new lines to EOF, advances `fileOffset = raf.getFilePointer()`.
- **Rotation/new file:** when the resolved path changes (new day or new session file), logs `watching <path>`, sets `currentFile` and resets `fileOffset = 0` (reads the new file from the top).
- **Truncation guard:** if `raf.length() < fileOffset`, resets offset to 0.
- **Encoding fix:** `RandomAccessFile.readLine()` decodes as ISO-8859-1; each line is re-decoded to UTF-8 (`new String(raw.getBytes(ISO_8859_1), UTF_8)`) before the consumer sees it.

`RollingLogTailer` holds **no** detection logic — the monitors own all parsing.

### `TellerSessionMonitor` — session state machine

Line format: `yyyy-MM-dd HH:mm:ss <message>`. Maintains single-customer session state (an IAT serves one customer at a time): `requestAt`, `sessionAt`, `videoUp`, `currentTeller`, plus a `logClock` (timestamp of the last processed line).

- `POST TellerSessionRequest message received` → `requestAt = ts` (idle `DELETE` heartbeats are ignored).
- A payload containing `"TellerSessionRequestId"` → session assigned (extracts `"TellerName"`; dedups repeated echoes of the same payload); clears `requestAt`, resets video flags.
- `OnAdd for RemoteControlSession` (while a session is active) → `videoUp = true`.
- `Ending teller session` / `OnDelete for TellerSessionRequest` → session end; if video never came up, emit `IAT_VIDEO_SESSION_FAIL`.
- REST outcome `(https?://…) server response (\w+)` → success statuses `{OK, Created, Accepted, NoContent, NotModified, ResetContent}` reset the failure streak (and, if `IAT_SERVER_UNREACHABLE` had been reported, emit `IAT_SERVER_RECOVERED`); anything else emits `IAT_SERVER_ERROR` and, at `failureThreshold` consecutive failures, `IAT_SERVER_UNREACHABLE` (once, until a success resets it).
- A payload containing `"ModeType"` with `"ModeName":"(\w+)"` → `IAT_MODE_CHANGE` (suppressed on first observation and on unchanged mode).
- **`checkTimers()`** runs after each batch, comparing `requestAt`/`sessionAt` against **`logClock`** (not wall-clock): overdue request → `IAT_TELLER_UNANSWERED`; session with no video past `videoTimeout` → `IAT_VIDEO_SESSION_FAIL`. Driving timers off the log clock means replaying a backlog can't raise spurious timeouts.

### `SymXchangeMonitor` — header-block parsing

The SymXchange log alternates header lines (`[yyyy-MM-dd HH:mm:ss-nnn][OpName]`) and XML payloads (PANs/SSNs masked to `****` at source). Per line:

- A header sets `currentTs`/`currentOp`, resets `faultReportedForBlock`, bumps `messageCount`.
- A `CONN_ERROR`-matching line (unable to connect / no connection / refused / timed out / DNS) increments a streak; at `failureThreshold` consecutive → `IAT_CORE_UNREACHABLE` (once until reset).
- Each SymXchange message **header** clears the connection-error streak — any response (incl. a fault) proves the core is reachable — and, if `IAT_CORE_UNREACHABLE` had been reported, emits `IAT_CORE_RECOVERED`.
- A `FAULT`-matching line (`faultstring`, `<faultcode`, `:Fault>`, `<Fault>`, `<ErrorMessage>`, non-zero `<ErrorCode>`) → `IAT_CORE_TXN_FAIL` (at most once per header block). The fault/conn regexes are conservative heuristics noted for tuning against a real failure sample.

### `CheckProcessingMonitor` — pipe-delimited records

Each `Process Date:`-prefixed line is split on `|` into `Key: Value` fields. A check is flagged (→ `IAT_CHECK_IMAGE_QUALITY`, informational) when any of: `Status` doesn't start with `Processed`; `Invalidity Score` > `invalidityThreshold`; or `|Amount CAR − Amount LAR| >= amountMismatchCents`. Amounts are in cents and rendered as dollars in the detail text. Clean checks emit nothing.

## How events reach the platform

All three monitors emit through the single shared `IatEventEmitter`, which wraps the same `IncidentEventClient` class used by `journal-events` and other event-reporting modules — see [technical.agent] for the full outbound API surface.

```
monitor → emitter.emit(eventType, details, eventSource, eventTime)
  → CreateJournalEventRequest.builder()
      .agentAtmId(atmId)            // ATM name string, resolved to numeric atmId server-side
      .eventType(eventType)         // free-form String, e.g. "IAT_CORE_UNREACHABLE"
      .eventDetails(details)
      .eventSource(eventSource)     // IAT_ACTIVETELLER | IAT_SYMXCHANGE | IAT_CHECK21
      .eventTime(ts.format(ISO_LOCAL_DATE_TIME) | null)
      .build()
  → IncidentEventClient.sendEvent(req)
      → HTTP POST {agent.endpoint or incident.endpoint}/api/journal-events
```

`eventType` is a plain `String` on `CreateJournalEventRequest` (the `hiveops-agent-journal` DTO) — not validated client-side. Nothing stops an unregistered `IAT_*` type from being sent; recognition/labeling happens server-side against hiveops-incident's DB-backed `event_type` table. On `IOException`, `emit()` logs a warning and drops the event — no retry, no queue.

## Platform abstraction

None used directly. The module reads plain files via `java.nio.file` / `RandomAccessFile` — no `PlatformService`/`PathResolver` dependency. The Hyosung/Windows constraint is enforced by deployment convention (installed only on `device.type=NH_IAT` machines) and the configured `activeteller.log.dir`, not by a runtime OS check in this module's code.

## Cross-links
- [technical.agent] — module system, lifecycle, shared HTTP client settings, full outbound API surface
- [platform.module-map]
