# `Statifier.Invoke.Answer`
[🔗](https://github.com/riddler/statifier-ex/blob/v2.6.0/lib/statifier/invoke/answer.ex#L1)

The two events that end a handler-backed invocation, built as plain
`Statifier.Event.t()` values by whoever is driving the chart.

An invocation started by `<invoke>` ends exactly two ways, and each way has
one event: `done.invoke.<invoke_id>` when the service finished
(ADR-0051 decision 5, spec 6.4.3), and
`error.communication.invoke.<invoke_id>` when the host's own retry policy
is exhausted and no done event will ever follow (ADR-0068). This module is
their single construction site.

## Why this is public

`Statifier.Session.done_invocation/3` and
`Statifier.Session.failed_invocation/3` are doors: they take a live
session, build the matching event here, and deliver it on an
invocation-tagged inbox entry. A host driving `Statifier.Interpreter`
directly against a persisted `%Statifier.MachineState{}`, with no
`Statifier.Session` process at all - `docs/persistence.md`'s supported
process-less path - has no session to hand those doors, so it had no way to
answer an invocation it started. Its next drive of the interpreter takes an
event, and this module is where that event comes from.

Both builders are pure: same arguments, same event, no process, no clock,
no id minting. `session_id` is the process-less host's own `_sessionid`
(spec 5.10) for the run - the same value it stamped onto the
`%MachineState{}` it is driving - and it appears only in `origin`, per C.1.

## Both halves or neither

These ship as a pair on purpose. ADR-0068 exists to remove the asymmetry
between an invocation that can report success and one that cannot report
failure, and its own "what would reopen this record" bullet names exactly
this gap for both events at once. A process-less host that could build one
and not the other would have the asymmetry back in a different place.

## What the caller still owns

The event is built here; nothing else is. A process-less host has no
invocation table for the library to pop and no drain to discard against, so
6.4.3's late-arrival discard - a done event for an invocation the chart
already cancelled - is the host's own check against its own record of which
invocations are live, exactly as `docs/durable-timers.md`'s Route B already
says for a timer that fires after its cancel. Feed the returned event to
the next drive only if the invocation is still live by the host's own
reckoning.

## `caller_context` is inherited, never minted

Both builders take an optional `caller_context:` (ADR-0063's opaque host
slot) and copy it onto the event they return, unread. It is the *invoking*
event's context - whatever opened the macrostep that executed the
`<invoke>` - not the answering call's own, which is why neither builder
reads an ambient value: the term travels from the invocation, and only a
caller holding the invocation can supply it. The Session path supplies it
from its own invocation table; a process-less host supplies what it stored
beside its invocation row (`docs/persistence.md`'s "Answering an
invocation with no session process"). Omitted, it is `nil` - ADR-0063's
own "no context attached", and the only value a run that never attached
one can produce (ADR-0063's 2026-09-02 decision note).

# `done`

```elixir
@spec done(
  session_id :: String.t(),
  invoke_id :: String.t(),
  donedata :: term(),
  opts :: keyword()
) :: Statifier.Event.t()
```

`done.invoke.<invoke_id>`: the invocation finished and `donedata` is what
it produced.

`donedata` is spec 6.4's own shape - the service's `<donedata>`, or
whatever a process-less host's equivalent is; 6.4's MUST there is on the
*service*, not this engine, which only builds the event and documents what
arrives in it. It reaches the chart as `_event.data`, and defaults to
`nil`.

The event carries `invokeid: invoke_id` (spec 5.10.1) so a chart reading
`_event.invokeid` sees which invocation answered, and `origin` /
`origintype` per C.1, exactly as `Statifier.Session.done_invocation/3`
builds them - the two paths call this same function, so their events are
byte-identical for the same arguments.

`opts` is read for one key, `:caller_context` - the invoking event's
ADR-0063 slot, copied onto the answer unread and defaulting to `nil` (see
the moduledoc's "`caller_context` is inherited, never minted").

    iex> event = Statifier.Invoke.Answer.done("sess_1", "inv_3", %{"outcome" => "approved"})
    iex> {event.name, event.type, event.data, event.invokeid}
    {"done.invoke.inv_3", :external, %{"outcome" => "approved"}, "inv_3"}

    iex> event = Statifier.Invoke.Answer.done("s1", "inv_3", nil, caller_context: {:trace, 7})
    iex> event.caller_context
    {:trace, 7}

# `failed`

```elixir
@spec failed(
  session_id :: String.t(),
  invoke_id :: String.t(),
  failure :: keyword(),
  opts :: keyword()
) :: Statifier.Event.t()
```

`error.communication.invoke.<invoke_id>`: the invocation failed
permanently and no `done.invoke.<invoke_id>` will ever follow.

The name is spec 3.12.1's blessed suffix extension of the
`error.communication` ADR-0051 decision 1's table already assigns to "a
registered handler fails to reach its service", so a chart transitioning on
`error.communication` (or on `error`) catches it by the descriptor prefix
rule with no edit, while a chart naming
`error.communication.invoke.<invoke_id>` parks that one invocation alone
(ADR-0068 decision 1).

`failure` is a keyword list read for three optional keys, none of which
this library interprets (ADR-0068 decision 2). The payload is a
string-keyed map, and an unsupplied key is `:undefined` (ADR-0037's unbound
spelling) rather than `nil`, which is distinct from a host that supplied a
zero or a null:

  * `:reason` - a host-chosen string naming the failure class, read from a
    chart as `_event.data.reason`. Defaults to `"unknown"`.
  * `:attempts` - how many attempts the host made before giving up.
  * `:detail` - any further host term, uninterpreted.

It is `Event.external/2` rather than `Event.platform/3` for decision 5's
reason: the processor detects nothing here, a host reports on an external
service's behalf, and the queue follows the arrival rather than the
`error.` prefix.

`opts` is separate from `failure` on purpose: `failure`'s three keys become
the chart-visible payload, and `:caller_context` is host plumbing the
datamodel never sees (ADR-0063 decision 2's last bullet). It is read for
that one key - the invoking event's slot, copied onto the answer unread and
defaulting to `nil`.

    iex> event = Statifier.Invoke.Answer.failed("sess_1", "inv_3", reason: "exhausted", attempts: 5)
    iex> {event.name, event.data}
    {"error.communication.invoke.inv_3",
     %{"reason" => "exhausted", "attempts" => 5, "detail" => :undefined}}

    iex> event = Statifier.Invoke.Answer.failed("s1", "inv_3", [], caller_context: {:trace, 7})
    iex> event.caller_context
    {:trace, 7}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
