Call lifecycle (Stasis)
This page preserves the merged Next documentation snapshot at e1ddcf6940adb7b619ce509063749e740eecc795. It can describe implementation beyond the source revision selected for the rebuilt Next guides. Retain the implementation-scope notes below and confirm your deployed version before applying these details. This is not a current-release availability or deployment claim. For the task-based entry point, see the rebuilt guide.
Delphi does not run voice logic inside Asterisk dialplan. Every call leg is routed into an Asterisk Stasis application and handed to TelPhi over the Asterisk REST Interface (ARI). TelPhi owns the entire conversation from the moment a channel enters Stasis until it is torn down. This page is the canonical reference for that lifecycle, including how the max-call-duration limit is armed and enforced.
For the surrounding media/SIP topology, see Voice service operations. For where the duration value comes from per flow, see Flow Settings — Call duration.
What Stasis is
Stasis is an ARI application — a named bucket (ARI_APP) that channels are sent to with the Stasis(<app>) dialplan application. While a channel is in a Stasis app, Asterisk emits events for that channel to the ARI client that has claimed the app, and the client can issue commands (play, record, hangup, bridge, …) on the channel. TelPhi is that ARI client.
The two events that bracket a call:
StasisStart— fired when a channel enters the Stasis app. This is TelPhi's "a call arrived" signal.StasisEnd— fired when a channel leaves Stasis (typically because it was hung up). This is TelPhi's "clean up this call" signal.
Lifecycle
1. StasisStart — resolve and arm
On StasisStart, TelPhi identifies the flow by the channel's entry point and looks up the prepared flow in Redis (lookupByPhone / lookupByEndpoint / lookupByApp / lookupByExt). The result is a PreparedFlow: the flow definition, resolved providers, audio plan, and the resolved max call duration + warning message.
TelPhi then:
- Wires the media plane (an Asterisk ExternalMedia channel bridged over a WebSocket) and builds the per-direction audio config.
- Starts the AI provider session (realtime, or a modular STT → LLM → TTS pipeline).
- Arms the max-duration timers (see below).
2. Conversation
The AI provider drives the conversation: caller speech is transcribed, turns are generated, and responses are played back. TelPhi serializes speech output and runtime commands so callers hear complete prompts before the line drops (see Speech playback and hangup).
3. StasisEnd — teardown
When the channel is hung up — by the caller, the AI, a transfer, the max-duration limit, or the bot no-input give-up — Asterisk fires StasisEnd. TelPhi's cleanup handler clears any armed timers, tears down the media session, and flushes the per-channel state from its in-memory map. The stashed termination reason is forwarded so it lands on the call detail record.
Max call duration limit
Each call on a flow has a maximum duration. When the cap is reached the channel is force-hung-up and the termination is classified as max_call_duration. The limit has two parts: where the value comes from, and how the timer is enforced.
Where the value comes from
The effective duration is resolved once, at flow-load time:
- Per-flow override —
sessionConfig.maxCallDurationSecondson the flow definition (set in Flow Settings → Call duration; defaults to 5400s / 90 min via the flow schema). This is the authoritative value when the flow author sets it. - Deployment fallback — when a flow leaves the duration unset, the env var
MAX_CALL_DURATION_SECONDS(default 7200s / 2h) is used.
The per-flow value is not clamped against the env value at runtime — the per-flow value overrides the deployment default outright. The flow schema validates the per-flow value's bounds on save.
The warning message follows the same precedence: per-flow sessionConfig.maxDurationWarningMessage first, then the env var MAX_CALL_DURATION_WARNING_MESSAGE, then nothing (silent hangup).
| Name | Source | Scope | Default | Description |
|---|---|---|---|---|
MAX_CALL_DURATION_SECONDS | env | voice | 7200 | Fallback max call duration (seconds) when a flow does not set its own. TelPhi force-hangs-up any call older than this and classifies the hangup as max_call_duration. A per-flow sessionConfig.maxCallDurationSeconds value overrides this. |
MAX_CALL_DURATION_WARNING_MESSAGE | env | voice | (empty) | Fallback warning text spoken ~10s before the max-duration hangup, used when a flow sets no warning of its own. Empty = silent hangup. A per-flow sessionConfig.maxDurationWarningMessage value takes precedence. |
How the timer is enforced
When a channel enters Stasis, TelPhi arms two timers from the resolved duration:
- a warning timer that fires roughly 10 seconds before the cap;
- a final timer that fires at the cap.
When the warning timer fires, TelPhi speaks the warning message (if any) and mutes caller input for the reminder. When the final timer fires, TelPhi stashes the termination reason max_call_duration and hangs the channel up through the normal termination path, so the reason propagates to the CDR.
Bot no-input give-up limit
Each call on a flow also has a bot no-input give-up watchdog (SCRUM-689). If the bot has not responded within the give-up window, the channel is either transferred (when sessionConfig.botNoInputGiveUpTransfer is set) or force-hung-up, and the termination is classified as bot_no_input_give_up. Unlike the failover watchdog (VGWK-25), this fires even when gatewayProfile.failover.enabled is false — it is a standalone, gateway-owned timer.
Status: Next / pending. This behaviour is proposed in platform PR #473 (internal implementation reference), which was still open and draft when reviewed on 2026-09-25, with no live-call acceptance recorded in this reference. Two SCRUM-689 requirements are not implemented: mid-call configuration of the give-up timeout, and the requested audio-file URL.
Where the value comes from
The effective give-up timeout is resolved once, at flow-load time — it is not reconfigured during a call. The bot-originated update_bot_delay_config event (the mid-call mechanism SCRUM-689 requires) updates only the failover watchdog's botNoInputGiveUpTimeoutMs, not this standalone timer:
- Per-flow override —
sessionConfig.botNoInputGiveUpTimeoutMSon the flow definition (set in Flow Settings → Call duration). This is the authoritative value when the flow author sets it. - Deployment fallback — when a flow leaves the timeout unset, the env var
BOT_NO_INPUT_GIVE_UP_TIMEOUT_MS(default 15000ms) is used.
The per-flow value is not clamped against the env value — it overrides the deployment default outright. Bounds are enforced differently per layer: the flow schema clamps the per-flow value into 1000–120000 ms on parse (so a legacy out-of-range flow still loads), the Flow Builder refuses to commit an out-of-range input, and the update_bot_delay_config runtime command rejects out-of-range values.
The give-up warning message follows the same precedence: per-flow sessionConfig.botNoInputGiveUpWarningMessage first, then the env var BOT_NO_INPUT_GIVE_UP_WARNING_MESSAGE, then nothing (silent termination). The warning is speech-synthesized text only; SCRUM-689's requested audio-file URL (botNoInputUrl parity) has no give-up equivalent — that option exists for the intermediate comfort prompt only.
The give-up terminal action is either a transfer or a hangup. When sessionConfig.botNoInputGiveUpTransfer is present, TelPhi transfers the call to its target (a phone number or sip:/sips: URI, with an optional routingPool egress route for number targets) via sip_refer; if the transfer fails, it falls back to the hangup. When the object is absent, the call is hung up directly. There is no env fallback for the transfer target — presence means transfer, absence means hangup. The transfer uses fixed sip_refer defaults.
How the timer is enforced
The give-up timer is re-armed on each bot request and cleared on each bot response (via the pipeline's onBotActivity hooks), so it measures bot responsiveness, not wall-clock call time. When it fires, TelPhi awaits the give-up warning playback (if configured), stashes the termination reason bot_no_input_give_up, and either transfers the call (when a transfer target is configured) or hangs the channel up through the normal termination path, so the reason propagates to the CDR. The playback wait is bounded (roughly the estimated audio duration plus 10 s, capped at 60 s); if the announcement fails or times out, TelPhi logs and continues to the terminal action, so the warning is best-effort rather than a guaranteed full playback. This ordering has not yet been verified against a live call — see the status note above.
In-memory only
The timers and per-channel state live in TelPhi process memory (an in-memory map keyed by channel id), not in Redis. If TelPhi restarts mid-call, the timers for active calls are lost. On restart, TelPhi reconciles its in-memory map against Asterisk ARI and reaps stale entries for channels that no longer exist; calls that are still alive continue without an armed duration timer for the remainder of that process lifetime (a new call after restart arms timers normally).
See also
- Voice service operations — TelSys/TelPhi topology and configuration.
- Flow Settings — Call duration — how an author sets the per-flow cap, give-up timeout, and warnings.
- Speech playback and hangup — TTS queueing and disconnect timing.
- Call detail records — where the
max_call_durationandbot_no_input_give_uptermination reasons surface. - Environment variable index — full env var reference.