Raxol.AgentClientProtocol.Session.Emitter.Journal (Raxol Agent Client Protocol v0.1.0)

Copy Markdown View Source

The durable emitter — reroutes updates and turn boundaries through the single-publisher Writer (Ext.Journal.Writer) so the session's durable, offset-addressed journal is FILLED by live turns, and every attached reattacher replays it.

What lands durably

  • emit/2 → Writer.append("session_update", SessionUpdate.to_json(update), taint). The Writer publishes the stamped record to every subscriber (reattachers). Taint is stamped here and NEVER filters delivery.
  • turn_started/3 → Writer.append("turn_started", %{turnId, prompt}, "user") — acquires the Writer's cross-connection turn latch.
  • turn_completed/3 → Writer.append("turn_completed", payload, "system") — clears the latch atomically. Built from the single render value, so a reattacher's turn_completed frame equals the live prompt response.

Delivery and edge cases

  • Origin delivery is append-then-notify (two call sites). emit/2 durably appends the update, then delivers it to the origin connection with a direct Connection.notify (the same FIFO lane as the prompt reply). Reattachers on OTHER connections get the identical stamped stream via the Writer subscription. Every delivered update is durable and reattachers see the same stream; append order is the single-publisher Writer's arrival order, so ordering is guaranteed for the Writer's sequence, NOT for interleaved live delivery from concurrent turn tasks.
  • No Writer ⇒ degrade to notify-only. If the session has no live Writer (Writer.whereis == nil), emit/2 still notifies (never crashes a turn); the update is simply non-durable. Turn boundaries no-op.
  • Born-cancelled turns append neither turn_started nor turn_completed: that path resolves in the Session without reaching the emitter.
  • prompt is recorded as []. turn_started records turnId (the turn-totality key) with an empty prompt-block list.