> For the complete documentation index, see [llms.txt](https://docs.amigo.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.amigo.ai/api-reference/change-logs/amigo-api/amigo-api-archive-v0-9-250-399.md).

# Archive: v0.9.250 - v0.9.399

Archived release history for the Amigo API. For current releases, see the [Amigo API changelog](/api-reference/change-logs/amigo-api.md).

<details>

<summary>v0.9.399 - Platform API: Atlas Handoff Prompting (July 2026)</summary>

#### Atlas Handoff Prompting

Atlas phase agents now receive explicit prompting that teaches the model how to advance through the conversation workflow using transfer tools. This fixes an issue where the voice model would remain in the entry phase without handing off, because it was never told that `transfer_to_<step>` tools are the mechanism to advance.

**What changed:**

* **Handoff system context.** Phase agents that have outgoing handoffs now receive a system context block explaining the multi-step call structure. The context tells the model it handles one step, that transfer tools are the only way to move the call forward, and that it should advance immediately once its step's goal is met. Agents without handoffs do not receive this block.
* **Directive transfer tool descriptions.** Each transfer tool's description is now written as a directive ("Hand the call off to the next step. Use this WHEN: ...") rather than a passive label. This helps the model understand when to invoke the tool.
* **Per-phase advancement instructions.** Each phase agent with outgoing handoffs receives a "How to advance" section appended to its instructions. This section lists every available transfer tool alongside its trigger condition as a capitalized directive, so the model has an unambiguous reference for when to call each transfer tool.
* **No mention of transfers to the caller.** The system context instructs the model never to mention transfers, connections, or handoffs to the caller - transitions happen silently in the background.

**What you need to do:**

* **No action required.** The improved prompting is applied automatically to all Atlas phase agents with outgoing handoffs. No configuration changes are needed.
* **Expect better handoff behavior.** If you observed Atlas calls stalling in the entry phase with zero handoffs, this update addresses the root cause. The model now reliably invokes transfer tools when a phase's goal is met.

</details>

<details>

<summary>v0.9.398 - Platform API: Atlas Global Tool Set and Handoff Observability (July 2026)</summary>

#### Atlas Global Tool Set and Handoff Observability

Atlas phase agents now receive the full platform tool set on every phase, and handoff events between phase agents are emitted to the call timeline for observability.

**What changed:**

* **Global tool set on every phase agent.** Previously, each phase agent received only the tools explicitly bound to its context graph state. In practice, context graph states often bind only a minimal set (or none), which left agents without the platform's globally available tools (skills and surface tools). Now every phase agent receives the full platform tool set in addition to any per-state bindings, matching how tools are exposed in other voice runtimes. Duplicate tools are deduplicated by name.
* **Handoff observability.** When one phase agent hands off to another, a state transition event is now emitted to the call timeline. This means phase-to-phase transitions appear in existing dashboards and call detail views alongside transcript and tool call events. Each handoff is also logged for diagnostics.
* **Agent activation logging.** When a phase agent becomes active (after a handoff or at call start), the event is logged for diagnostics.

**What you need to do:**

* **No action required.** Phase agents automatically receive the full tool set and handoff events appear in the call timeline. No configuration changes are needed.
* **Review call timelines.** If you use Atlas, call detail views now show state transition events for phase handoffs, giving you visibility into how the agent navigates the conversation workflow.

</details>

<details>

<summary>v0.9.397 - Platform API: Atlas Voice Runtime (July 2026)</summary>

#### Atlas Voice Runtime

A new voice session runtime - Atlas - is now available as an opt-in selection for voice agents. Atlas compiles the agent's context graph into a graph of self-sequencing phase agents connected by native handoffs, so the conversation workflow is driven by structured agent collaboration rather than a single flattened prompt. Atlas ships dark by default and is selected only through explicit runtime configuration.

**What changed:**

* **New `atlas` session provider.** The voice configuration now accepts `atlas` as a session provider value alongside the existing `inhouse` and real-time speech-to-speech options. The runtime is selected per service, per agent, or per workspace, with inheritance up the configuration hierarchy (service > agent > workspace > environment default).
* **Context graph compiled to agent graph.** When Atlas owns a call, it compiles the context graph into a set of phase agents - each representing a conversation phase with its own objectives, guidelines, and available tools. Agents hand off to each other using native handoffs derived from the context graph's exit conditions. This preserves the context graph as the authoring surface while giving the voice runtime structured multi-agent collaboration.
* **Degraded single-agent fallback.** If the agent engine is unavailable at call start, Atlas falls back to a single agent with all available tools and no handoffs. The call stays alive rather than dropping.
* **Barge-in and interruption handling.** Atlas supports server-side voice activity detection with interrupt-and-respond behavior. When the caller interrupts, stale audio from the previous response is suppressed and the telephony output buffer is flushed.
* **Transcript and tool call observability.** Caller and agent transcripts, tool call start/complete events, barge-in events, and time-to-first-audio latency are emitted through the standard call timeline, so existing dashboards and call detail views work without changes.
* **Token metering.** Per-response token counts are recorded for observability, tagged by provider and workspace. Full billing metering is deferred to a future release.
* **Dark by default.** Atlas is not enabled unless explicitly configured. Existing voice agents continue to use their current runtime with no change in behavior.

**What you need to do:**

* **No action required for existing agents.** Atlas is opt-in only. Your current voice configuration is unaffected.
* **To try Atlas:** Set the session provider to `atlas` in your service, agent, or workspace voice configuration. Atlas requires a context graph to compile its agent graph - agents without a context graph will run in degraded single-agent mode.
* **Review context graph structure.** Atlas derives its phase agents and handoffs from the context graph. Clear step objectives, action guidelines, and exit conditions improve the quality of the compiled agent graph.

</details>

<details>

<summary>v0.9.396 - Platform API: Per-Model Region Routing for Voice Agent (July 2026)</summary>

#### Per-Model Region Routing for Voice Agent

The voice agent engine now performs per-model region routing for supported model families. Newer model versions that are served exclusively on a global endpoint are automatically routed there, while older versions in the same family continue to use the workspace's configured regional endpoint.

**What changed:**

* **Automatic global endpoint routing.** Models in certain families that require a global endpoint (such as newer-generation models) are now routed to that endpoint transparently. The platform detects the model version and selects the correct endpoint without any configuration changes.
* **Regional endpoint preserved for older models.** Older model versions that support regional endpoints continue to use the workspace's configured region. There is no change to data residency behavior for these models.
* **Dual-client architecture.** The voice agent engine maintains separate connections for regional and global endpoints, selecting the appropriate one per request based on the model being invoked. This ensures that routing decisions are made per-call rather than per-session.

**What you need to do:**

* **No action required.** Region routing is automatic based on the model configured for your agent. Workspaces using older model versions see no change in behavior. Workspaces using newer model versions that were previously unavailable on regional endpoints can now use them without manual endpoint configuration.

</details>

<details>

<summary>v0.9.395 - Platform API: Voice Agent Single-Agent Workflow (July 2026)</summary>

#### Voice Agent Single-Agent Workflow

Real-time voice sessions now use a single-agent workflow model. The voice agent receives the entire conversation workflow as a structured prompt and reasons through all steps itself, with the full tool set always available. This replaces the previous per-state gating approach (v0.9.394) where tools and persona changed at each state transition - that approach caused the voice model to resist transitions and created an impedance mismatch between fine-grained state control and real-time conversation flow.

**What changed:**

* **Whole-workflow prompt.** The voice agent receives a single system prompt that includes the agent's persona, the full conversation workflow rendered as an ordered list of step objectives and guidelines, global guidelines, and global constraints. The agent reasons through the steps in order rather than being gated to one state at a time.
* **Full tool set always available.** All of the agent's configured tools are declared at session start and remain available throughout the call. The agent decides when to use each tool based on the workflow context. There is no per-state restriction on which tools appear.
* **Workflow steps derived from context graph.** The workflow steps in the prompt are generated from the context graph's engageable states (action and data collection states), ordered by conversation flow. Each step includes the state's objective and any action guidelines. The context graph remains the authoring source of truth for conversation design.
* **Safety model.** Write safety is enforced by the prompt (which describes the workflow and when writes are appropriate), the tools the agent is given, and the existing write deduplication on the execution layer. There is no real-time human approval gate on the voice path - an approval step in the hot path would stall a live call.
* **No per-state transitions.** The synthetic state transition mechanism from v0.9.394 has been removed. The voice agent no longer signals explicit state transitions, and no transition events are emitted during voice calls.
* **Parallel tool calls disabled.** Voice sessions continue to execute tool calls sequentially (one at a time), preventing duplicate parallel invocations.

**What you need to do:**

* **No action required for existing agents.** Voice sessions automatically use the single-agent workflow model. Agents without context graphs are unaffected.
* **Review context graph step objectives.** The voice agent uses step objectives and action guidelines from the context graph to understand the workflow. Clear, descriptive objectives improve the agent's ability to work through the call correctly.
* **Exit condition descriptions are no longer used for voice transitions.** If you previously tuned exit condition descriptions specifically for voice transition accuracy (per v0.9.394 guidance), those descriptions no longer affect voice sessions. They remain relevant for text session navigation.

</details>

<details>

<summary>v0.9.394 - Platform API: Voice Agent Per-State Gating (July 2026) [Reverted in v0.9.395]</summary>

#### Voice Agent Per-State Gating

{% hint style="warning" %}
**Reverted.** This release was reverted in v0.9.395. Voice sessions now use the single-agent workflow model described above. The per-state gating behavior described below is no longer active.
{% endhint %}

Real-time voice sessions now constrain the agent's persona and available tools to the current conversation state, matching the state-aware behavior of text sessions. Previously, voice sessions exposed the full tool set for the entire call. Now, tools and persona instructions update automatically as the conversation advances through the context graph.

**What changed:**

* **State-gated tools and persona.** When a voice call starts, the agent's persona and available tools are derived from the starting state in the context graph. Only tools authorized by the current state are available to the agent - a write tool does not appear until the conversation reaches a state that authorizes it.
* **Model-driven state transitions.** The voice agent can advance through context graph states during the call. When the agent determines the current state's objective is complete, it signals a transition to the next state. The platform validates the transition against the context graph's exit conditions, reconfigures the session with the new state's persona and tools, and the agent continues in the new scope. Invalid transitions are rejected and the agent stays in the current state.
* **Routing state resolution.** Annotation and decision states in the context graph are resolved automatically during transitions, so the agent always lands on an actionable state. Decision states are resolved using the same navigation logic as text sessions.
* **Terminal state handling.** When the conversation reaches the terminal state in the context graph, all tools are removed and the agent delivers a closing message without starting new topics or invoking tools.
* **State transition events.** Each state transition emits a `state_transition` event to the call trace, recording the new state and whether it is terminal. These events appear in the call timeline alongside existing transcript and tool call events.
* **Defense in depth.** Per-state gating is the primary access control for tools in voice sessions. Existing write gates (approval policies, write deduplication) continue to apply underneath as defense in depth.
* **No free-form tool fallback.** Voice sessions without a co-located agent engine run without tools rather than falling back to the full tool set. This closes the previous gap where all tools could be exposed before state gating was in place.

**What you need to do:**

* **No action required for existing agents.** Voice sessions automatically use per-state gating when the agent has a context graph configured. Agents without context graphs are unaffected.
* **Review context graph exit conditions.** The voice agent uses exit condition descriptions to decide when and where to transition. Clear, descriptive exit conditions improve transition accuracy. If your context graph has ambiguous or missing exit condition descriptions, consider updating them.
* **Parallel tool calls disabled.** Voice sessions now execute tool calls sequentially (one at a time) to ensure clean state transitions. This may affect agents that previously relied on parallel tool execution in voice sessions.

</details>

<details>

<summary>v0.9.393 - Platform API: Voice Agent Function Calling and Token Metering (July 2026)</summary>

#### Voice Agent Function Calling and Token Metering

The real-time voice agent now supports function calling and per-response token metering, allowing voice agents to invoke the same tools available to text conversations and providing visibility into token consumption.

**What changed:**

* **Function calling in voice sessions.** Voice agents can now call the same tools (skills, integrations, platform functions) available to text sessions. When the agent's tool configuration is available, the voice session declares those tools to the voice model. The model can invoke tools mid-conversation, and results are returned to the model so it can continue speaking with the tool's output. Tool execution runs concurrently with audio processing, so a slow tool does not block audio playback or caller interruptions.
* **Observer trace for tool calls.** Each tool invocation emits `tool_call_started` and `tool_call_completed` events to the call trace, including the tool name, execution duration, and success status. These events appear in the call timeline and playground trace alongside existing transcript and barge-in events.
* **Graceful tool failure.** If a tool execution fails, the voice agent returns a structured error to the model rather than dropping the call. The model can recover and continue the conversation. Failed tool calls are logged in the call trace with failure status.
* **Per-response token metering.** Each voice model response now emits token usage counts (input and output tokens) to the monitoring pipeline. Token counts are tagged by workspace for per-tenant cost visibility. This is observability-only - billing integration is a future phase.
* **Tool-less fallback.** If the agent's tool configuration is unavailable (for example, due to a transient error during call setup), the voice session runs without tools rather than failing the call. The agent continues to handle the conversation without tool capabilities.
* **Clean teardown of in-flight tools.** When a call ends, any tool executions still in progress are cancelled immediately, preventing tools from running side effects after the call has concluded.

**What you need to do:**

* **No action required for existing agents.** If your agent has tools configured, voice sessions will automatically make them available. Agents without tools are unaffected.
* **Review tool behavior for voice.** Tools that work in text sessions will work in voice sessions using the same execution path. However, tools with long execution times may cause brief pauses in agent speech. Consider this when designing tools for voice-heavy workflows.

</details>

<details>

<summary>v0.9.392 - Platform API: Per-Agent Persona and Language for Voice Sessions (July 2026)</summary>

#### Per-Agent Persona and Language for Voice Sessions

Real-time voice sessions now use the agent's configured persona and spoken language, so each voice agent behaves according to its own identity settings rather than using a generic default.

**What changed:**

* **Agent persona in voice sessions.** Voice sessions now receive the agent's configured persona (service description, capabilities, and behavioral instructions) automatically. Previously, voice sessions used a generic default persona. The persona is derived from the agent's static identity configuration - it does not change based on the caller's current position in the conversation flow.
* **Agent spoken language in voice sessions.** The agent's configured default spoken language is now forwarded to voice sessions automatically. The language value is normalized from its stored format to a standard two-letter code (e.g. `en`, `es`, `fr`) before being applied. Supported language codes include `en`, `es`, `fr`, `de`, `pt`, `ar`, `zh`, `ja`, and `ko`. If no language is configured on the agent, the session uses the default language.
* **Consistent session preparation.** Both persona and language are resolved during call preparation and remain available throughout the voice session. This eliminates cases where a session could miss the agent's identity configuration after internal routing changes.

**What you need to do:**

* **No action required for existing agents.** If your agent already has a persona and spoken language configured, voice sessions will automatically use them.
* **To customize voice agent behavior,** configure the agent's identity settings (persona description and default spoken language) through the agent configuration. Voice sessions will reflect these settings on the next call.

</details>

<details>

<summary>v0.9.391 - Platform API: Identity Binding Test Values for Custom Token Exchange Integrations (July 2026)</summary>

#### Identity Binding Test Values for Custom Token Exchange Integrations

Custom token exchange integrations now support optional test values for identity-bound parameters, enabling workspace authors to test identity-bound integration endpoints from the Developer Console without requiring an external user session.

**What changed:**

* **New optional field: `identity_binding_test_values`.** When configuring a custom token exchange integration, you can now provide a map of test values for identity-bound parameters. Test values are only supported for `external_user.subject_key` bindings - other binding types are rejected at configuration time.
* **Author-time validation.** Test value keys must reference parameters declared in `identity_bindings`, and those bindings must use the `external_user.subject_key` attribute path. Values must be non-empty strings of at most 512 characters.
* **Gated runtime use.** Test values are used only during workspace-authenticated text conversation turns where no external user identity is present and the actor holds integration view permissions. They are never used on voice paths, external-user sessions, or principal bindings. When a test value is used, the platform logs the event for observability.
* **Fail-closed behavior preserved.** If a required identity attribute is unavailable and no gated test value applies, the integration call is refused rather than executed with an absent identity.

**What you need to do:**

* **No action required for existing integrations.** The field is optional and defaults to an empty map. Existing integrations are unaffected.
* **To configure test values,** add `identity_binding_test_values` to your custom token exchange auth configuration, mapping each identity-bound parameter name to a test string. Use this to test identity-bound endpoints from workspace-authenticated text conversations in the Developer Console without needing an external user session.

</details>

<details>

<summary>v0.9.390 - Platform API: Voice Agent Observability and Transcripts (July 2026)</summary>

#### Voice Agent Observability and Transcripts

The real-time voice agent now emits observability events and call transcripts, bringing its trace output in line with the in-house voice pipeline.

**What changed:**

* **Time-to-first-audio latency.** The voice agent now measures and reports time-to-first-audio (the delay between call start and the first agent audio reaching the caller). This metric appears on the same monitoring dashboards as the in-house pipeline's setup delay metric. The call timeline displays real latency numbers rather than missing values.
* **Barge-in events.** Each caller interruption emits a trace event recording which speaker was interrupted, which speaker interrupted, and a running count of barge-ins for the call. These events appear in the call timeline and playground trace.
* **Caller transcripts.** The voice agent now emits caller speech transcripts to the call trace. Transcripts are generated as a side-channel for observability - the voice model hears the audio directly and does not depend on these transcripts for comprehension.
* **Agent transcripts.** The voice agent now emits agent speech transcripts to the call trace, so the full conversation text is available in the call timeline and analytics pipeline.
* **Privacy-safe logging.** Transcript content is delivered through the trace channel but is not written to application logs. Log entries record transcript length only, with no speech content.

**What you need to do:**

* **No action required.** Observability events and transcripts are emitted automatically for all real-time voice sessions. Call timelines, playground traces, and analytics pipelines pick up the new events without configuration changes.

</details>

<details>

<summary>v0.9.389 - Platform API: Voice Agent Barge-In, Greeting, and Language Control (July 2026)</summary>

#### Voice Agent Barge-In, Greeting, and Language Control

The real-time voice agent now supports caller barge-in (interruption handling), automatic inbound greetings, and per-agent language configuration.

**What changed:**

* **Barge-in support.** When a caller speaks while the agent is responding, the agent stops its current reply immediately. Buffered agent audio is flushed from the telephony path so the caller does not hear stale output after interrupting. The agent then responds to the caller's new input rather than continuing the interrupted reply.
* **Inbound greeting.** On inbound calls, the agent now greets the caller immediately when the call connects rather than waiting for the caller to speak first. This reduces perceived startup delay and establishes a natural call opening. Outbound calls are not affected - outbound greeting orchestration is handled separately.
* **Language configuration.** Voice agents now accept a `language` parameter in their provider configuration. When set to a supported language code (e.g. `en`, `es`, `fr`, `de`, `pt`, `ar`, `zh`, `ja`, `ko`, `hi`, `it`), the agent responds exclusively in that language and does not switch languages based on caller accent or occasional foreign-language words. The default is `en` (English). Setting the value to `multilingual` or an unmapped code removes the language restriction, allowing the agent to respond in multiple languages.
* **Voice selection.** Voice agents now accept a `voice` parameter in their provider configuration to select the agent's speaking voice.
* **Updated default model.** The default real-time voice model has been updated to the latest generally available version.
* **Telephony-optimized audio input.** Voice input now uses noise reduction optimized for close-talking telephony (phone held to the ear), improving turn detection accuracy on phone calls.

**What you need to do:**

* **No action required for existing agents.** Barge-in, inbound greeting, and English language default are enabled automatically.
* **To change the agent's response language,** set the `language` field in the voice provider configuration to a supported language code.
* **To change the agent's voice,** set the `voice` field in the voice provider configuration.
* **To enable multilingual responses,** set `language` to `multilingual` or omit a single-language restriction.

</details>

<details>

<summary>v0.9.388 - Platform API: Toll-Free Verification Privacy, Terms, and Age-Gated Content Fields (July 2026)</summary>

#### Toll-Free Verification Privacy, Terms, and Age-Gated Content Fields

Toll-free verification submissions now require privacy policy and terms-and-conditions URLs, plus an age-gated content declaration. These fields are submitted to the carrier during verification and returned on the verification response.

**What changed:**

* **New required field: `privacy_policy_url`.** A public URL pointing to the business's privacy policy. Must be a valid HTTP(S) URL.
* **New required field: `terms_and_conditions_url`.** A public URL pointing to the business's terms and conditions. Must be a valid HTTP(S) URL.
* **New required field: `age_gated_content`.** A boolean indicating whether the messaging campaign carries age-gated content.
* **Response includes new fields.** The toll-free verification response object now includes `privacy_policy_url`, `terms_and_conditions_url`, and `age_gated_content`.
* **Fields submitted to carrier.** All three fields are forwarded to the carrier as part of the toll-free verification request, both for new submissions and resubmissions of previously rejected verifications.

**What you need to do:**

* **Add `privacy_policy_url`, `terms_and_conditions_url`, and `age_gated_content` to all toll-free verification submissions.** These fields are now required. Provide publicly accessible URLs for your privacy policy and terms, and indicate whether your messaging content is age-gated.
* **Update integrations that read verification responses.** The response now includes `privacy_policy_url`, `terms_and_conditions_url`, and `age_gated_content`. Update your response parsing to handle these fields.

</details>

<details>

<summary>v0.9.387 - Platform API: SMS Opt-In Consent for US/CA Toll-Free Numbers (July 2026)</summary>

#### SMS Opt-In Consent for US/CA Toll-Free Numbers

The SMS opt-in and send flows now support US and Canadian toll-free numbers alongside the existing US A2P 10DLC long-code consent path. The opt-in endpoint sends from the matched compliance line rather than through the Messaging Service.

**What changed:**

* **Opt-in now supports toll-free numbers.** `POST /v1/sms/opt-in` resolves the opt-in copy and sender from whichever compliance artifact backs the SMS use case. If the use case has a verified A2P campaign with an assigned US long-code, the campaign's registered opt-in message is sent from that long-code. Otherwise, if the use case has an assigned US or Canadian toll-free number with an approved toll-free verification, the verification's opt-in confirmation message is sent from that toll-free line. If neither is present, the endpoint returns 422.
* **Opt-in sends from the matched line.** The opt-in message is now sent from the specific compliance line (`from_`) rather than through the Messaging Service SID. This ensures the recipient sees the same number that will send future messages.
* **Send gate expanded to toll-free.** `POST /v1/sms/send` now requires a recorded opt-in for US/CA toll-free sender pools in addition to US long-code pools. If the recipient has no opt-in record for the use case and the sender pool contains US/CA toll-free or US long-code numbers, the endpoint returns 422.
* **Updated 422 descriptions.** The 422 error descriptions for both endpoints now reflect the broader consent requirement covering both US/CA toll-free and US long-code sender pools.

**What you need to do:**

* **Send opt-in before messaging from toll-free numbers.** If your SMS use case sends from US or Canadian toll-free numbers, call `POST /v1/sms/opt-in` for each recipient before sending messages. This was previously only required for US long-code (A2P 10DLC) sender pools.
* **No changes needed for existing A2P workflows.** If you already send opt-in messages for US long-code use cases, those workflows continue to work as before.
* **No changes needed for international or Canadian long-code pools.** Canadian long-codes and international numbers are not subject to the opt-in gate.

</details>

<details>

<summary>v0.9.386 - Platform API: Toll-Free Verification Business Registration Fields (July 2026)</summary>

#### Toll-Free Verification Business Registration Fields

Toll-free verification submissions now require business registration fields that describe the legal structure and registration details of the business behind the messaging program.

**What changed:**

* **New required field: `business_type`.** Every toll-free verification submission must now include a `business_type` field indicating the legal structure of the business. Accepted values: `PRIVATE_PROFIT`, `PUBLIC_PROFIT`, `SOLE_PROPRIETOR`, `NON_PROFIT`, `GOVERNMENT`.
* **New conditionally required fields: `business_registration_number`, `business_registration_authority`, `business_registration_country`.** For every `business_type` except `SOLE_PROPRIETOR`, all three business registration fields are required. `business_registration_number` is the legally recognized registration number (max 64 characters). `business_registration_authority` is the issuing authority (accepted values: `EIN`, `CBN`, `CRN`, `PROVINCIAL_NUMBER`, `VAT`, `ACN`, `ABN`, `BRN`, `SIREN`, `SIRET`, `NZBN`, `USt-IdNr`, `CIF`, `NIF`, `CNPJ`, `UID`, `NEQ`, `OTHER`). `business_registration_country` is the ISO 3166-1 alpha-2 country code of registration.
* **Conditional validation.** Submitting a `business_type` other than `SOLE_PROPRIETOR` without all three registration fields returns a 422 error. Sole proprietors may omit the registration fields.
* **Response includes new fields.** The toll-free verification response object now includes `business_type` (always present), `business_registration_number`, `business_registration_authority`, and `business_registration_country` (null when `business_type` is `SOLE_PROPRIETOR`).

**What you need to do:**

* **Add `business_type` to all toll-free verification submissions.** This field is now required. Choose the value that matches your business's legal structure.
* **Add business registration fields for non-sole-proprietor submissions.** If your `business_type` is anything other than `SOLE_PROPRIETOR`, include `business_registration_number`, `business_registration_authority`, and `business_registration_country` in your submission.
* **Update integrations that read verification responses.** The response now includes `business_type`, `business_registration_number`, `business_registration_authority`, and `business_registration_country`. Update your response parsing to handle these fields.

</details>

<details>

<summary>v0.9.385 - Platform API: Console Intake Catalog and Schema Registry (July 2026)</summary>

#### Console Intake Catalog and Schema Registry

The Platform API now includes console-facing endpoints for managing customer data intake - a file catalog with upload, download, and listing, plus a schema registry for defining dataset contracts. All endpoints use standard workspace session (JWT) authentication.

**What changed:**

* **New endpoint: `GET /v1/{workspace_id}/intake/files`.** List uploaded files with pagination, sorting, status filtering, and filename search. Each file shows its dataset, schema version, validation status, size, and upload timestamp. Files that failed validation include a human-readable error reason.
* **New endpoint: `POST /v1/{workspace_id}/intake/files`.** Upload a file to a registered dataset via multipart form data. The platform validates the file synchronously against the dataset's schema contract and assigns a terminal status (success or failed) on upload. Byte-identical re-uploads to the same dataset are deduplicated and return the existing file. Maximum upload size is 100 MB.
* **New endpoint: `GET /v1/{workspace_id}/intake/files/{file_id}/download`.** Download the original uploaded file bytes by file ID.
* **New endpoint: `GET /v1/{workspace_id}/intake/datasets`.** List registered schemas with pagination, sorting, and search by dataset name.
* **New endpoint: `POST /v1/{workspace_id}/intake/schema/register`.** Register a new schema contract for a dataset. A schema defines the expected file format (CSV), primary key columns, and typed field definitions (string, integer, float, boolean, date, datetime). Dataset names must be unique within a workspace.
* **External integration upload path relocated.** The HMAC-authenticated external integration upload endpoint has moved from `POST /v1/{workspace_id}/intake/files` to `POST /v1/{workspace_id}/intake/files/external` to accommodate the new console upload endpoint.
* **Upload validation.** Uploaded CSV files are validated against the dataset's schema: UTF-8 encoding, header presence, required columns, per-cell type coercion, and non-empty primary key values. Validation stops at the first violation and reports the error inline on the file record.
* **Malware scanning.** All uploads are scanned before processing. If the scanner is unavailable, uploads are rejected (fail-closed).
* **Versioning.** Each uploaded file receives a monotonically increasing version number within its dataset. Concurrent uploads receive distinct versions.

**What you need to do:**

* **Update external integration upload paths.** If your integration uploads files via the HMAC-authenticated path at `POST /v1/{workspace_id}/intake/files`, update the URL to `POST /v1/{workspace_id}/intake/files/external`. The request format and authentication are unchanged.
* **No action needed for console users.** The new file catalog and schema registry endpoints are available immediately for workspace session-authenticated callers.

</details>

<details>

<summary>v0.9.384 - Platform API: Per-Service and Per-Agent Voice Session Provider Selection (July 2026)</summary>

#### Per-Service and Per-Agent Voice Session Provider Selection

The voice session runtime - which controls whether a call uses the default in-house audio pipeline or a speech-to-speech runtime - can now be configured per-service and per-agent, instead of only as a global default.

**What changed:**

* **New field: `voice_config.session_provider` on agents.** Agent voice configuration now accepts an optional `session_provider` field. Accepted values are `inhouse` (the default audio pipeline) and `openai_realtime` (speech-to-speech runtime). When set, this overrides the global default for calls handled by that agent. When `null` or omitted, the agent inherits the global setting.
* **New field: `voice_config.session_provider` on services.** Service voice configuration now accepts the same optional `session_provider` field. This is the highest-precedence tier - when set, it overrides both the agent-level and global settings. When `null` or omitted, the service inherits from the agent or global default.
* **Tiered precedence.** Resolution follows a precedence ladder: service overrides agent, agent overrides global default. At each tier, a `null` or omitted value means "inherit from the tier below." This mirrors the existing precedence model used for TTS provider selection.
* **No breaking changes.** Existing configurations are unaffected. Both fields default to `null`, which preserves the current behavior (global default applies).

**What you need to do:**

* **No action required.** Existing agents and services continue to use the global default. To override the voice session runtime for a specific agent or service, set `voice_config.session_provider` to `inhouse` or `openai_realtime` on the agent or service configuration.

</details>

<details>

<summary>v0.9.383 - Platform API: Toll-Free Verification Business Identity Sourced from Customer Profile (July 2026)</summary>

#### Toll-Free Verification Business Identity Sourced from Customer Profile

Toll-free verification submissions no longer accept or return business identity fields (address and contact information). Business identity is now sourced exclusively from the verified Customer Profile attached to the setup.

**What changed:**

* **Business identity fields removed from the submission endpoint.** The following form fields are no longer accepted when submitting a toll-free verification: `business_street_address`, `business_street_address2`, `business_city`, `business_state_province_region`, `business_postal_code`, `business_country`, `business_contact_first_name`, `business_contact_last_name`, `business_contact_email`, `business_contact_phone`. The carrier sources this information from the verified Customer Profile linked to the setup.
* **Business identity fields removed from the response object.** The toll-free verification response no longer includes `business_street_address`, `business_street_address2`, `business_city`, `business_state_province_region`, `business_postal_code`, `business_country`, `business_contact_first_name`, `business_contact_last_name`, `business_contact_email`, or `business_contact_phone`. The `additional_information` field remains.
* **Customer Profile is the source of truth.** Business identity for toll-free verifications is determined by the Customer Profile associated with the setup. Submitting business address or contact fields alongside a Customer Profile is not supported by the carrier and was previously error-prone.

**What you need to do:**

* **Remove business identity fields from toll-free verification submissions.** If your integration submits toll-free verifications, remove all business address and contact fields from the request. These fields are now rejected. Ensure your Customer Profile on the setup contains accurate business identity information.
* **Update integrations that read verification responses.** If you parse the toll-free verification response, remove references to the business address and contact fields. Only `additional_information` remains as an optional free-text field.

</details>

<details>

<summary>v0.9.382 - Platform API: Toll-Free Verification Enum Updates (July 2026)</summary>

#### Toll-Free Verification Enum Updates

Toll-free verification submissions now use updated enum values that align with the carrier API vocabulary for opt-in types, use-case categories, and message volume buckets.

**What changed:**

* **New opt-in type: `IMPORT`.** The `IMPORT` opt-in type is now accepted for toll-free verification submissions, representing a bulk-import opt-in workflow.
* **Use-case categories updated.** The accepted use-case category values have changed to match the carrier's API codes. The previous human-readable category names (e.g. "2FA", "General Marketing", "Fraud Alerts") are replaced with standardized codes (e.g. `TWO_FACTOR_AUTHENTICATION`, `MARKETING`, `FRAUD_ALERT_MESSAGING`). The full set of accepted values is: `TWO_FACTOR_AUTHENTICATION`, `ACCOUNT_NOTIFICATIONS`, `CUSTOMER_CARE`, `CHARITY_NONPROFIT`, `DELIVERY_NOTIFICATIONS`, `FRAUD_ALERT_MESSAGING`, `EVENTS`, `HIGHER_EDUCATION`, `K12`, `MARKETING`, `POLLING_AND_VOTING_NON_POLITICAL`, `POLITICAL_ELECTION_CAMPAIGNS`, `PUBLIC_SERVICE_ANNOUNCEMENT`, `SECURITY_ALERT`.
* **Message volume buckets updated.** The highest volume bucket has changed from `5,000,000+` to two separate buckets: `5,000,000` and `10,000,000+`.

**What you need to do:**

* **Update use-case categories in toll-free verification submissions.** If your integration submits toll-free verifications, replace the previous human-readable category values with the new standardized codes listed above. Submissions using the old category names will be rejected.
* **Update message volume values if applicable.** If you were using `5,000,000+`, switch to either `5,000,000` or `10,000,000+` depending on your actual volume.
* **No action needed for opt-in types** unless you want to use the new `IMPORT` opt-in type for bulk-import workflows.

</details>

<details>

<summary>v0.9.381 - Platform API: Opt-In Confirmation Message on Toll-Free Verification (July 2026)</summary>

#### Opt-In Confirmation Message on Toll-Free Verification

Toll-free verification submissions now require an opt-in confirmation message - the SMS text sent to confirm a recipient's opt-in.

**What changed:**

* **New required field: `opt_in_confirmation_message`.** The toll-free verification submission endpoint now requires an `opt_in_confirmation_message` field (string, 1-160 characters). This is the SMS message sent to recipients to confirm their opt-in, and it is forwarded to the carrier as part of the verification request.
* **Returned in verification responses.** The `opt_in_confirmation_message` field is included in the toll-free verification response object, so you can confirm what was submitted.
* **Stored on the verification record.** The confirmation message is persisted alongside other verification fields and is included when retrieving existing verifications.

**What you need to do:**

* **Include `opt_in_confirmation_message` in toll-free verification submissions.** All new submissions must include this field. The value must be between 1 and 160 characters. This is the exact SMS text your recipients receive to confirm their opt-in.
* **Update integrations that read verification responses.** The response object now includes the `opt_in_confirmation_message` field.

</details>

<details>

<summary>v0.9.380 - Platform API: Operator Endpoint to Clear SMS Setup Opt-Outs (July 2026)</summary>

#### Operator Endpoint to Clear SMS Setup Opt-Outs

Operators can now clear a setup-wide SMS opt-out for a recipient, re-enabling sends to that phone number on the setup. This is the only path to reverse a STOP today - there is no recipient-initiated re-subscribe-via-text flow that clears setup-level opt-outs.

**What changed:**

* **New endpoint: clear SMS setup opt-out.** A new POST endpoint allows operators to remove a live setup-wide SMS opt-out for a given setup and phone number. The request requires a reason (1-2000 characters, whitespace-trimmed) that serves as the durable audit record of why the STOP was reversed.
* **204 on success.** When a live opt-out exists and is successfully cleared, the endpoint returns 204. The recipient can be sent to again on this setup.
* **404 when no live opt-out exists.** If no live setup-wide opt-out exists for the given setup and phone number, the endpoint returns 404.
* **409 when a toll-free opt-out is still active.** If the recipient still has an active opt-out against a US/CA toll-free number under the same setup, the endpoint returns 409. Toll-free opt-outs have a clean recipient-initiated reversal path (the recipient texts START or UNSTOP to the toll-free number), so the operator must route the recipient through that first. Once the toll-free opt-out is cleared by the recipient, the operator can retry the setup opt-out clear.
* **Audit trail.** The reason supplied by the operator is persisted alongside the soft-delete timestamp as the permanent record of why the opt-out was reversed. There is no separate audit log - the reason on the cleared record is the audit trail.

**What you need to do:**

* **No action required for existing integrations.** This is a new endpoint. Existing SMS send behavior is unchanged.
* **To clear a setup opt-out:** POST to the new endpoint with the setup ID, recipient phone number (E.164 format), and a reason explaining why the STOP is being reversed (for example, out-of-band proof of fresh consent). Handle 404 (no live opt-out) and 409 (toll-free opt-out still active) responses.
* **Toll-free opt-outs must be cleared by the recipient first.** If the endpoint returns 409, instruct the recipient to text START or UNSTOP to the toll-free number, then retry.

</details>

<details>

<summary>v0.9.379 - Platform API: US/CA Toll-Free SMS Opt-Out Management (July 2026)</summary>

#### US/CA Toll-Free SMS Opt-Out Management

The platform now manages STOP/START/UNSTOP keyword handling for US and CA toll-free SMS numbers. Opt-out enforcement has also been expanded so that all SMS number types - not just A2P 10DLC long codes - block sends to recipients who have opted out.

**What changed:**

* **Toll-free STOP/START/UNSTOP handling.** Inbound STOP, START, and UNSTOP keywords from recipients on US/CA toll-free numbers are now processed by the platform. A STOP creates an opt-out record that blocks further sends to that recipient. START or UNSTOP reverses a live opt-out. These keyword messages are consumed by the platform and never forwarded to the agent.
* **Opt-out enforcement applies to all number types.** Previously, the opt-out send gate only applied to A2P 10DLC long-code sends. It now applies to all SMS sends regardless of number type (toll-free, long code, short code). If a recipient has opted out for the active use case, the send endpoint returns 422 with a message indicating the recipient has opted out.
* **No change to recipient-initiated re-subscription.** A recipient who has sent STOP can re-subscribe by sending START or UNSTOP to the same toll-free number. Operator-driven opt-out reversal remains unsupported.
* **Idempotent keyword processing.** Repeated STOP messages from an already opted-out recipient, or START/UNSTOP messages from a recipient who is not opted out, are accepted silently with no state change. The keyword is still suppressed from the agent in all cases.

**What you need to do:**

* **No action required for most integrations.** If you send SMS through the platform, opt-out enforcement is automatic. Sends to opted-out recipients now return 422 regardless of number type.
* **Update error handling if needed.** If your integration previously only expected 422 opt-out errors on A2P 10DLC sends, be aware that the same error can now occur on toll-free sends.

</details>

<details>

<summary>v0.9.378 - Platform API: Opt-In Resend Can No Longer Reverse SMS Opt-Out (July 2026)</summary>

#### Opt-In Resend Can No Longer Reverse SMS Opt-Out

The SMS opt-in resend endpoint no longer supports reversing a recipient's opt-out (STOP). Once a recipient opts out, re-subscription requires a fresh, recipient-initiated consent path - an operator-driven resend cannot override it.

**What changed:**

* **`reverses_opt_out` field removed.** The opt-in resend request no longer accepts the `reverses_opt_out` boolean field. Previously, setting this to `true` allowed an opt-in resend to reverse a live opt-out. This parameter has been removed from the request schema.
* **409 is now unconditional on opt-out.** If the recipient has opted out (a prior STOP), the endpoint always returns 409 Conflict. There is no way to override this through the opt-in resend endpoint.
* **Re-subscription requires recipient-initiated consent.** To re-subscribe a recipient who has opted out, a fresh consent path initiated by the recipient is required. Operator-driven resends cannot reverse a STOP.
* **Idempotent behavior unchanged.** If the recipient is already opted in, the endpoint still returns 201 with no message re-sent (no-op).

**What you need to do:**

* **Remove `reverses_opt_out` from opt-in resend requests.** If your integration passes `reverses_opt_out` in the request body, remove it. The field is no longer accepted.
* **Update opt-out handling workflows.** If you relied on `reverses_opt_out=true` to re-subscribe contacts who sent STOP, you must implement a recipient-initiated consent path instead. The platform no longer permits operator-driven opt-out reversal.

</details>

<details>

<summary>v0.9.377 - Platform API: Per-Skill Sampling Parameters (July 2026)</summary>

#### Per-Skill Sampling Parameters

Skills now support optional `temperature` and `top_p` fields for controlling response randomness and diversity on a per-skill basis.

**What changed:**

* **`temperature` field on skills.** Skills now accept an optional `temperature` field (number, 0-1) on create and update. Controls sampling temperature for the skill's model. Returned in skill responses.
* **`top_p` field on skills.** Skills now accept an optional `top_p` field (number, 0-1) on create and update. Controls nucleus sampling for the skill's model. Returned in skill responses.
* **Model-gated behavior.** Sampling parameters are model-gated - models that do not support sampling parameters ignore them silently rather than returning an error. No action is needed for skills targeting these models.
* **Mutual exclusivity.** Set at most one of `temperature` or `top_p`. If both are provided, the runtime keeps `temperature` and drops `top_p`. Models that reject receiving both parameters are handled automatically.
* **Defaults.** Both fields default to `null` (not set), which preserves the model's default sampling behavior. Existing skills are unaffected.

**What you need to do:**

* **No action required for existing skills.** Both fields default to `null`, so existing skills behave exactly as before.
* **To use sampling parameters:** Add `temperature` or `top_p` (not both) to your skill create or update request. Values must be between 0 and 1 inclusive.

</details>

<details>

<summary>v0.9.376 - Platform API: External User Subject Key Binding for Integration Auth (July 2026)</summary>

#### External User Subject Key Binding for Integration Auth

Integration identity bindings now support the `external_user.subject_key` binding path, allowing custom token exchange integrations to inject the verified external user's subject key into auth parameters without requiring a materialized session principal.

**What changed:**

* **New `external_user.subject_key` binding.** Custom token exchange integrations can now bind a declared auth parameter to `external_user.subject_key` in addition to the existing `principal.subject_key` and `principal.subject_id` paths. The new binding resolves from the external-user authentication context verified at token issuance, so external-user sessions can use per-user integration auth without an entity-anchored role assignment.
* **External subject key on session tokens.** External-user session tokens now carry the subject key provided at token issuance. The subject key is included in access tokens, refresh tokens, and token rotation, and is validated (non-empty string, max 512 characters) at issuance time.
* **External subject key on text interactions.** The text interaction request now accepts an optional `external_subject_key` field (string, max 512 characters) that is forwarded to the agent engine for identity binding resolution.
* **Fail-closed binding resolution.** If an integration declares an `external_user.*` identity binding but the session has no external-user context, or declares a `principal.*` binding but the session has no materialized principal, the integration call is refused with a structured error. Integrations without identity bindings are unaffected.
* **Identity binding validation updated.** The `identity_bindings` field on custom token exchange auth configuration now accepts `external_user.subject_key` as a valid binding target alongside the existing `principal.subject_key` and `principal.subject_id` values.

**What you need to do:**

* **To use the new binding:** Update your integration's `identity_bindings` to map a declared param name to `external_user.subject_key`. Ensure that external-user session tokens are issued with a subject key (the `external_subject_key` field on the session grant request).
* **Existing integrations are unaffected.** Integrations using `principal.*` bindings continue to work as before. Integrations with no identity bindings are unchanged.

</details>

<details>

<summary>v0.9.375 - Platform API: Reversible SMS Opt-Out via Opt-In Resend (July 2026)</summary>

#### Reversible SMS Opt-Out via Opt-In Resend

The SMS opt-in endpoint now supports reversing a prior STOP (setup-wide opt-out) when resending an opt-in message, and the opt-out model uses soft-delete semantics so the full opt-out history is retained for audit.

**What changed:**

* **`reverses_opt_out` field on opt-in request.** The send opt-in message endpoint now requires a `reverses_opt_out` boolean. When `true` and the recipient has a live opt-out (a prior STOP), the opt-out is reversed before the opt-in message is sent. When `false` and an opt-out exists, the endpoint returns 409. This field is required so callers explicitly acknowledge re-subscribing a contact who previously opted out.
* **Opt-out reversal is soft-delete.** Reversing an opt-out does not destroy the original opt-out record. The record is marked as reversed, and a subsequent STOP from the same recipient creates a new opt-out record. This preserves the full compliance history for audit.
* **Idempotent opt-in resend.** If the recipient is already opted in for the use case, the endpoint returns 201 with no message re-sent. Any opt-out reversal requested in the same call is still applied, so the reversal persists even when consent was already on file.
* **409 response updated.** The 409 response now indicates that the recipient has a live setup-wide opt-out and `reverses_opt_out` is false, rather than indicating a duplicate opt-in.
* **Opt-out check on send.** The SMS send endpoint now checks only live (non-reversed) opt-outs when determining whether a recipient is blocked. Previously reversed opt-outs no longer prevent sending.

**What you need to do:**

* **Add `reverses_opt_out` to opt-in requests.** The field is required. Set it to `true` when you intend to re-subscribe a contact who previously sent STOP. Set it to `false` if you want the request to fail when an opt-out exists.
* **Update 409 handling.** If your integration handles the 409 response from the opt-in endpoint, update your logic to reflect the new meaning: the recipient has a live opt-out and `reverses_opt_out` was not set to true.

</details>

<details>

<summary>v0.9.374 - Platform API: Feature Flag Targeting Key and Unconditional Provider Registration (July 2026)</summary>

#### Feature Flag Targeting Key and Unconditional Provider Registration

The platform's feature flag evaluation now accepts an optional targeting key and always registers the flag provider regardless of environment.

**What changed:**

* **Targeting key support.** Feature flag evaluations now accept an optional `targeting_key` parameter that identifies the entity the flag is evaluated against. The targeting key enables consistent percentage-rollout bucketing and sticky per-entity flag assignment. When omitted, evaluations behave as before.
* **Unconditional provider registration.** The feature flag provider is now registered in all environments. Previously, provider initialization was skipped in local and dev environments, causing all evaluations to return defaults. The provider is now always initialized, and whether evaluations resolve from targeting rules or return the passed default is controlled by the deployment environment's runtime configuration - not by application-level gating.

**What you need to do:**

* **No action required for existing integrations.** Existing flag evaluations continue to work without changes. The targeting key is optional - omitting it preserves current behavior.
* **Pass a targeting key for per-entity rollouts.** If you use percentage-based rollouts or per-entity flag assignment, pass the relevant entity identifier as the targeting key to get consistent bucketing.

</details>

<details>

<summary>v0.9.373 - Platform API: A2P Campaign Subscriber Opt-In Simplified (July 2026)</summary>

#### A2P Campaign Subscriber Opt-In Simplified

The A2P campaign submit request and campaign response no longer include a `subscriber_opt_in` field. The platform always collects consent and sends the opt-in message as the first message before any other message on the use case, so subscriber opt-in is always true. The field has been removed from the request and response to reflect this.

**What changed:**

* **`subscriber_opt_in` removed from submit request.** The A2P campaign submit endpoint no longer accepts `subscriber_opt_in` as a request field. The platform always sets subscriber opt-in to true when registering the campaign.
* **`subscriber_opt_in` removed from campaign response.** The A2P campaign response no longer includes `subscriber_opt_in`. All campaigns are treated as having subscriber opt-in.
* **`opt_in_message` is always required.** The `opt_in_message` field was already required, but its role is now clearer: the platform sends `opt_in_message` to a recipient as the first message before any other message on the use case, so every campaign must carry it.
* **`opt_in_keywords` clarified.** The `opt_in_keywords` field controls text-to-join keyword opt-in (e.g. "START"). It can be empty when the campaign collects consent through other channels such as web or paper forms.

**What you need to do:**

* **Remove `subscriber_opt_in` from campaign submit requests.** If your integration sends `subscriber_opt_in` when submitting A2P campaigns, remove it. The field is no longer accepted.
* **Update response parsing.** If your code reads `subscriber_opt_in` from campaign responses, remove that field access. The field is no longer present.

</details>

<details>

<summary>v0.9.372 - Platform API: SMS Opt-In Enforcement and STOP/HELP Opt-Out Management (July 2026)</summary>

#### SMS Opt-In Enforcement and STOP/HELP Opt-Out Management

The channel service now enforces A2P opt-in consent for US SMS numbers and handles STOP/HELP keyword management directly, replacing carrier-level handling with platform-managed compliance.

**What changed:**

* **Opt-in consent endpoint.** A new SMS opt-in endpoint sends the A2P campaign's registered consent message to a recipient phone number for a given use case. The opt-in is recorded per use case and phone number. Resending an opt-in to a number that has already received one returns 409. The endpoint requires a verified A2P campaign to be associated with the use case.
* **Send-time opt-in enforcement.** Outbound SMS sends to US numbers with A2P campaign management now require that the opt-in consent message has been sent for the use case and phone number combination. Sends to numbers without a prior opt-in are rejected with a 422 error.
* **Send-time opt-out enforcement.** Outbound SMS sends now check whether the recipient has opted out. Sends to opted-out numbers are rejected with a 422 error rather than failing at the carrier level.
* **STOP keyword handling.** When a recipient texts a registered opt-out keyword (e.g. STOP), the platform records the opt-out, replies with the registered opt-out confirmation message, and does not forward the message to the agent. Opt-outs apply across all SMS use cases under the same business identity.
* **HELP keyword handling.** When a recipient texts a registered help keyword (e.g. HELP), the platform replies with the registered help message and does not forward the message to the agent.
* **Non-keyword messages unchanged.** Inbound SMS messages that do not match opt-out or help keywords continue to be forwarded to the agent as normal conversation turns.

**Error responses on the opt-in endpoint:**

| Status | Condition                                                                                                                                                                 |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 404    | SMS use case not found                                                                                                                                                    |
| 409    | Opt-in already sent for this use case and phone number                                                                                                                    |
| 422    | No verified A2P campaign, invalid destination (landline, malformed, premium-rate), recipient opted out, destination region not permitted, or brand registration suspended |

**Error responses on the send endpoint (new conditions):**

| Status | Condition                                                      |
| ------ | -------------------------------------------------------------- |
| 422    | Opt-in message not yet sent for this use case and phone number |
| 422    | Destination phone number has opted out of receiving messages   |

**What you need to do:**

* **Send opt-in messages before sending SMS.** For US numbers with A2P campaign management, call the opt-in endpoint for each use case and recipient phone number before sending messages. Sends without a prior opt-in will be rejected.
* **Handle new 422 error responses.** Update your error handling for the SMS send endpoint to handle the new opt-in and opt-out enforcement errors.
* **No changes needed for non-US numbers or use cases without A2P campaigns.** The opt-in and opt-out enforcement only applies to use cases with verified A2P campaigns.

</details>

<details>

<summary>v0.9.371 - Platform API: Unified Email Read Surface and Simplified Send Paths (July 2026)</summary>

#### Unified Email Read Surface and Simplified Send Paths

The channel service email endpoints have been consolidated into a single namespace. Inbound and outbound emails are now served from one set of read endpoints, and send paths use cleaner route conventions.

**What changed:**

* **Unified email list and detail endpoints.** The list endpoint now returns both inbound and outbound emails in a single chronological feed, discriminated by a `direction` field on each item. You can filter to one direction with the optional `direction=inbound|outbound` query parameter. The separate `/inbound` list endpoint has been removed.
* **Unified get-one endpoint.** The detail endpoint returns a discriminated response keyed on `direction`. Inbound emails include fields such as received timestamp, body kind, reply linkage, and attachment filenames. Outbound emails include delivery state, engagement counts, and event timestamps. The separate `/inbound/{email_id}` detail endpoint has been removed.
* **Unified body, attachment, and raw endpoints.** The `/body`, `/attachment/{filename}`, and `/raw` sub-resource endpoints now serve both inbound and outbound emails by ID. The `/raw` endpoint returns the original MIME bundle for inbound emails only (outbound emails do not retain a raw bundle and return 404). The separate `/inbound/{email_id}/body`, `/inbound/{email_id}/attachment/{filename}`, and `/inbound/{email_id}/raw` endpoints have been removed.
* **Peer address filter.** The `to_address` filter on the list endpoint has been renamed to `peer_address`. For outbound emails it matches the recipient; for inbound emails it matches the sender. Set semantics (any match) are preserved.
* **Reply linkage filter.** A new `in_reply_to_email_id` filter on the list endpoint lets you find emails that are replies to a specific outbound or inbound email.
* **Simplified send paths.** The email send endpoint has moved from `POST .../send` to `POST .../` on the email namespace. The iMessage send endpoint has similarly moved from `POST .../send` to `POST .../` on the iMessage namespace. Request and response shapes are unchanged.

**What you need to do:**

* **Update any calls to the removed inbound endpoints.** Replace calls to the `/inbound`, `/inbound/{email_id}`, `/inbound/{email_id}/body`, `/inbound/{email_id}/attachment/{filename}`, and `/inbound/{email_id}/raw` endpoints with the corresponding unified endpoints. Use the `direction` query parameter or the `direction` field on the response to distinguish inbound from outbound.
* **Update the `to_address` filter.** If you use the `to_address` query parameter on the email list endpoint, rename it to `peer_address`.
* **Update send endpoint paths.** If you call the email or iMessage send endpoints directly, update the path to remove the `/send` suffix.
* **Handle the discriminated response shape.** The list and detail endpoints now return items with a `direction` field. Update your client models to handle both the inbound and outbound response variants.

</details>

<details>

<summary>v0.9.370 - Platform API: Inbound SMS via Channel Manager (July 2026)</summary>

#### Inbound SMS via Channel Manager

The platform now drives inbound SMS conversations end-to-end through the channel manager path, alongside the existing iMessage and email channels.

**What changed:**

* **Inbound SMS processing (channel-manager path).** Inbound SMS messages arriving through the channel manager are now fully processed by the agent engine. Messages from the same caller on the same use case are serialized onto a single durable conversation, matching the session model used by iMessage. The agent's reply is delivered as plain text.
* **Distinct from classic SMS path.** This channel-manager SMS path is keyed on the use case and uses the channel manager's sender pool. It is separate from the classic SMS service path, which routes inbound messages using phone number mappings and processes turns within the SMS service. Both paths coexist - the routing depends on how the inbound message arrives.
* **Session routing.** Channel-manager SMS conversations use a routing key derived from the caller and use case, so repeat messages from the same caller on the same use case resume the existing conversation rather than starting a new one.

**What you need to do:**

* **To receive inbound SMS through the channel manager,** create a use case service binding with channel `sms` for the relevant service. Inbound SMS messages matching the bound use case will be routed to the agent automatically.
* **No changes needed for existing SMS integrations.** If you use the classic SMS service with phone number mappings, that path continues to work unchanged. The two SMS paths are independent.

</details>

<details>

<summary>v0.9.369 - Platform API: iMessage Inbound Turns and Proactive Outbound iMessage (July 2026)</summary>

#### iMessage Inbound Turns and Proactive Outbound iMessage

The platform now drives inbound iMessage conversations end-to-end and supports proactive (agent-initiated) iMessage sends through the outbound text endpoint.

**What changed:**

* **Inbound iMessage processing.** Inbound iMessages are now fully processed by the agent engine. Messages from the same caller on the same use case are serialized onto a single durable conversation using a two-hour conversation window, matching the phone-shaped session model used by SMS. The agent's reply is delivered as plain text with no subject line or unsubscribe footer.
* **Proactive outbound iMessage.** The `/create_outbound_text` endpoint now accepts `channel_kind='imessage'`. When set, the agent-initiated message is delivered through the channel's managed sender pool resolved from the use case. The `use_case_id` field is **required** when `channel_kind` is `imessage`.
* **Use case service binding channels expanded.** Use case service bindings now accept `sms` and `imessage` as binding channels, in addition to the existing `inbound_voice`, `outbound_voice`, `ringless_voicemail`, and `email` channels. These bindings allow the channel service's inbound webhooks to resolve incoming SMS and iMessage turns to the correct workspace.
* **Use case list filter expanded.** The `channel` query parameter on the list use cases endpoint now accepts `sms` and `imessage` as filter values.

**What you need to do:**

* **To receive inbound iMessages,** create a use case service binding with channel `imessage` for the relevant service. Inbound iMessages matching the bound use case will be routed to the agent automatically.
* **To send proactive iMessages,** call `/create_outbound_text` with `channel_kind='imessage'` and include the `use_case_id` of the use case whose sender pool should be used. The `service_number` field is not used for iMessage delivery.
* **Update client models if needed.** If your client validates the binding channel enum or the `channel_kind` field, add `sms` and `imessage` as accepted values.

</details>

<details>

<summary>v0.9.368 - Platform API: Per-Tenant Intake Volume Routing (July 2026)</summary>

#### Per-Tenant Intake Volume Routing

Customer data intake uploads can now be routed to dedicated per-tenant storage volumes instead of the shared documents volume. Routing is controlled by a global configuration flag (default OFF) with an exclusion list for workspaces that need to remain on the legacy layout. No API changes are required for callers.

**What changed:**

* **Per-tenant volume routing.** When enabled, new file uploads are written to a dedicated volume scoped to the workspace. Files are stored by date partition and file ID only - filenames and customer identifiers are not included in the storage path, reducing exposure of patient-identifiable information in storage infrastructure.
* **Global-on with exclusion list.** When the global flag is ON, all workspaces route to per-tenant volumes except those on the exclusion list. The exclusion list holds workspaces whose downstream pipeline triggers still depend on the legacy storage layout and need coordinated re-pointing before they can move. Once a workspace's triggers are updated, it is removed from the exclusion list. The exclusion list is environment-agnostic - a workspace identifier that does not exist in a given environment is simply never matched, so the same list works across staging and production.
* **Backward-compatible reads.** File downloads, metadata lookups, and deletions automatically resolve the correct storage layout based on the stored path. Files written under the previous shared-volume layout continue to work without modification or migration. A mixed state where some workspaces use per-tenant and others use shared is always safe.
* **No blob rewrite required.** Existing files remain in place. The platform resolves both layouts transparently, so enabling per-tenant routing for new uploads does not affect access to historical files.
* **Startup validation.** The intake volume configuration, including the exclusion list, is validated at service startup to catch misconfigurations before they affect uploads. Malformed workspace identifiers in the exclusion list are rejected at startup rather than silently ignored.

**What you need to do:**

* **No action required for existing integrations.** The flag defaults to OFF and the exclusion list defaults to empty, so current behavior is unchanged. File upload and download APIs are unaffected.
* **To enable per-tenant routing for all workspaces,** coordinate with your deployment team to ensure per-tenant volumes are provisioned before enabling the global flag. File-arrival triggers for downstream pipelines must also be updated to watch the new volume locations.
* **To hold specific workspaces on the legacy layout,** add their workspace identifiers to the exclusion list before enabling the global flag. This keeps those workspaces on the shared layout until their pipeline triggers are re-pointed, while all other workspaces - including any newly created ones - automatically route to per-tenant volumes.
* **If you previously used the per-workspace allow-list,** note that the routing model has changed from opt-in (allow-list) to opt-out (exclusion list). The global flag now controls the default for all workspaces, and only workspaces that cannot move yet need to be listed. Update your configuration accordingly.

</details>

<details>

<summary>v0.9.367 - Platform API: MMS as a First-Class Phone Number Capability (July 2026)</summary>

#### MMS as a First-Class Phone Number Capability

Phone number provisioning and search now surface MMS as a first-class capability alongside voice and SMS.

**What changed:**

* **MMS capability on phone numbers.** Provisioned phone numbers now report `mms` in their capabilities list when the underlying number supports MMS. Previously, only `voice` and `sms` were tracked.
* **Search by MMS capability.** The list available phone numbers endpoint now accepts `mms` in the `capabilities` filter. When specified, only numbers that support MMS are returned.
* **Capability set expanded.** The phone number capability type now includes `mms` in addition to `voice` and `sms`. Existing numbers that support MMS will reflect this capability after re-provisioning or on new provisioning requests.

**What you need to do:**

* **No action required for existing integrations.** Existing phone numbers continue to work as before. The `mms` capability will appear on newly provisioned numbers that support it.
* **To filter by MMS support,** include `mms` in the `capabilities` parameter when searching for available phone numbers.
* **Update client models if needed.** If your client strictly validates the capability enum, add `mms` as an accepted value.

</details>

<details>

<summary>v0.9.366 - Platform API: Inbound MMS Support and Attachment Access (July 2026)</summary>

#### Inbound MMS Support and Attachment Access

The SMS channel now supports receiving MMS messages with media attachments. Inbound media is persisted, exposed through the existing attachment endpoint, and forwarded to the agent as file content parts.

**What changed:**

* **Inbound MMS media handling.** When an inbound SMS includes media (MMS), the platform downloads and stores each attachment. Media filenames and content types are preserved. The agent receives each attachment as a `file` content part alongside any text body.
* **`body` is now optional on inbound messages.** A captionless MMS (media with no text) stores the body as null rather than an empty string, consistent with outbound MMS behavior.
* **`attachment_names` on inbound get-message response.** The get-message response for inbound messages now includes an `attachment_names` field (array of strings) listing the filenames of any received media attachments.
* **`num_attachments` on inbound list-messages response.** The list-messages response now includes a `num_attachments` field on inbound messages, reflecting the count of received media attachments. Previously, inbound messages always showed 0.
* **Attachment download works for inbound messages.** `GET /v1/sms/{sms_id}/attachment/{attachment_name}` now supports both outbound and inbound messages. If the message ID matches an inbound message with the given attachment name, the endpoint returns a redirect to a short-lived download URL. Returns 404 if neither an outbound nor inbound message with that attachment is found.
* **`is_mms` filter applies to inbound messages.** The `is_mms` query parameter on the list-messages endpoint now filters inbound messages by attachment presence, consistent with outbound filtering.
* **Inbound MMS media cleanup.** After the inbound message is persisted and the agent turn is driven, the platform removes the provider-hosted copy of the media on a best-effort basis. The platform's own copy is retained for audit and retrieval.

**What you need to do:**

* **No action required for existing integrations.** Text-only inbound SMS continues to work as before.
* **Update client models for nullable `body`.** The `body` field on inbound messages is now nullable. If your client parses this field strictly, update your model to accept null values.
* **To access inbound attachments,** use the existing `GET /v1/sms/{sms_id}/attachment/{attachment_name}` endpoint with the inbound message ID and attachment name from the `attachment_names` field.

</details>

<details>

<summary>v0.9.365 - Platform API: Outbound MMS Support on SMS Send (July 2026)</summary>

#### Outbound MMS Support on SMS Send

The SMS send endpoint now supports sending MMS messages with media attachments alongside or instead of text content.

**What changed:**

* **MMS support on `POST /v1/sms/`.** The send endpoint now accepts multipart form data with optional `media_attachments` (up to 10 files per message). Including any media attachment sends the message as MMS. At least one of `message` (text body) or `media_attachments` is required.
* **`body` is now optional.** The `message` field on the send request is no longer required. You can send a media-only MMS with no text body. The `body` field in responses is now nullable (null for media-only messages).
* **Attachment metadata on responses.** The get-message response now includes an `attachment_names` field (array of strings) listing the filenames of any media attachments on the message.
* **`num_attachments` on list responses.** The list-messages response now includes a `num_attachments` field (integer) on each outbound message, indicating how many media attachments are present.
* **New `is_mms` query filter.** The list-messages endpoint accepts a new `is_mms` boolean query parameter to filter for messages with attachments (`true`) or text-only messages (`false`).
* **New attachment download endpoint.** `GET /v1/sms/{sms_id}/attachment/{attachment_name}` returns a redirect to a short-lived download URL for a specific attachment on an outbound message. Returns 404 if the message or attachment name is not found.
* **Capability validation.** MMS sends require at least one MMS-capable phone number in the sender pool. Text-only sends require at least one SMS-capable phone number. Sends return 422 if the pool lacks the required capability.
* **Attachment validation.** Each attachment must have a valid filename (max 20 characters), be between 1 byte and 600 KB in size, and have a unique name within the message. Total message size (text body plus all attachments) must be under 5 MB. Invalid attachments return 422 with a descriptive error.

**What you need to do:**

* **No action required for existing SMS integrations.** Text-only SMS sends continue to work as before. The `body` field is now nullable in responses - update your client models if you parse this field strictly.
* **To send MMS,** switch your send request to multipart form encoding and include files in the `media_attachments` field. Ensure your sender pool includes at least one MMS-capable phone number.
* **To download attachments,** use the new `GET /v1/sms/{sms_id}/attachment/{attachment_name}` endpoint. The response is a 307 redirect to a short-lived download URL.

</details>

<details>

<summary>v0.9.364 - Platform API: External-User Self-Approval for Conversations (July 2026)</summary>

#### External-User Self-Approval for Conversations

External users can now approve or reject a parked, approval-gated write in their own conversation without waiting for an operator to act through the review queue.

**What changed:**

* **New `POST /v1/{workspace_id}/conversations/{conversation_id}/approval` endpoint.** External users can submit an approve or reject decision for a write that the agent paused for confirmation in their own conversation. The request body accepts a `decision` field (`approve` or `reject`) and an optional `reason` (1-512 characters). Returns 204 on success.
* **New `conversations:approve_own` scope.** A new scope has been added to the set of scopes available for external user tokens. This scope is required to call the self-approval endpoint. It is included in the default external user scope set.
* **Ownership enforcement.** Only the external user who owns the conversation can submit a decision. Requests for conversations owned by other users return 404. Requests from non-external-user tokens (API keys, role-based principals) return 403.
* **Durable decision recording.** The decision is durably recorded and consumed by the agent on the conversation's next turn, consistent with the transport-agnostic approval resume behavior used by operator review.
* **503 on store unavailability.** If the decision store is temporarily unavailable, the endpoint returns 503 rather than 500, signaling that the request is safe to retry.

**What you need to do:**

* **No action required for existing integrations.** This is a new endpoint and scope. Existing operator review-queue workflows are unaffected.
* **To enable self-approval for external users,** ensure the external user token includes the `conversations:approve_own` scope. This scope is included in the default external user scope set, so tokens issued with default scopes already have it.
* **Client integration.** To use self-approval, POST to the new endpoint with the external user's token when a parked write requires confirmation. The agent will consume the decision on the next turn.

</details>

<details>

<summary>v0.9.363 - Platform API: Patient-Facing URL on Surface GET Response (July 2026)</summary>

#### Patient-Facing URL on Surface GET Response

The GET surface endpoint now returns a `url` field containing a patient-facing link that operators can copy and share directly.

**What changed:**

* **New `url` field on surface responses.** The GET surface response now includes a `url` field (string or null). The URL is minted on each read, so operators can retrieve and share the link from the read view without needing to re-create the surface.
* **Null when a URL cannot be generated.** If the surface lacks the required entity or expiry information, or if the workspace is not configured for token minting, the `url` field is returned as `null`.
* **No changes to create or update.** The `url` field is read-only and does not appear on create or update request bodies. It is computed on retrieval only.

**What you need to do:**

* **No action required.** This is a non-breaking addition. Existing integrations will see the new `url` field in GET surface responses. You can use this field to display or share the patient-facing link directly from your application.

</details>

<details>

<summary>v0.9.362 - Platform API: Live Model Catalog Validation on Service Configuration (July 2026)</summary>

#### Live Model Catalog Validation on Service Configuration

Service create and update endpoints now validate model IDs in version set preferences against the set of models the platform account can actually serve. This catches within-family typos (such as a misspelled model variant) at configuration time rather than letting them degrade at runtime.

**What changed:**

* **Model ID validation against a live catalog.** When you create or update a service with `version_sets`, the platform checks each model ID that routes through the primary LLM provider against a cached catalog of models the account can access. A model ID that is structurally valid (passes the prefix check) but does not appear in the catalog is rejected with `422 Unprocessable Entity`. The error message lists the offending version set entries and model IDs.
* **Fail-open behavior.** If the live catalog is temporarily unavailable (cold cache, transient infrastructure issue), the validation is skipped and the request falls back to the existing structural prefix check. A catalog gap never blocks a legitimate configuration write - this is important because writing a new-model configuration is how an operator adopts a just-released model.
* **Non-primary-provider models are unaffected.** Models that route to other provider families are not covered by this catalog and continue to use the structural prefix gate only.
* **Background catalog refresh.** The model catalog is refreshed automatically on a background loop. The cached catalog has a TTL significantly longer than the refresh interval, so a single failed refresh never expires the cache - readers keep the last-good set until a refresh succeeds.

**What you need to do:**

* **No action required for correctly configured services.** If your version sets reference valid, currently-served model IDs, nothing changes.
* **Fix any typos in model IDs.** If a service create or update starts returning `422` with a message about model IDs not served for your account, check the listed model IDs for typos or retired model variants. The error message identifies the exact version set entry and model ID that failed validation.
* **Newly released models.** If you are adopting a model that was just released and the catalog has not refreshed yet, the validation will fail open (skip the catalog check) and allow the write. You do not need to wait for a catalog refresh.

</details>

<details>

<summary>v0.9.361 - Platform API: Cached Principal Resolution with Automatic Invalidation (July 2026)</summary>

#### Cached Principal Resolution with Automatic Invalidation

External principal resolution is now cached per workspace so that per-turn authorization checks during a conversation do not require a database read. Mutations to role assignments, role grants, and external roles automatically invalidate the workspace's cached principals, so changes take effect on the next conversation turn.

**What changed:**

* **Per-turn principal lookups are now cached.** When a conversation turn resolves the session's external principal, the result is read from a short-lived per-workspace cache instead of the database. Cache misses trigger a database load and populate the cache for subsequent turns. Sessions without a principal (the normal patient path) also cache the no-principal result, avoiding unnecessary lookups.
* **Automatic invalidation on mutation.** Creating, updating, or deleting a role assignment (via the External Role Assignments endpoints), creating or deleting a role grant (via the Role Grants endpoints), or deleting an external role (via the External Roles endpoints) now automatically invalidates all cached principals for the affected workspace. The next conversation turn re-resolves from the database and sees the updated state.
* **Fail-closed on cache unavailability.** If the cache is temporarily unreachable, the platform falls back to a direct database load. Authorization decisions remain fail-closed - a cache miss never results in elevated access. Cache write failures are non-fatal and do not affect the mutating API request.
* **TTL-bounded staleness.** Cached entries carry a short TTL. Even if an invalidation is missed due to a transient infrastructure issue, the cached principal expires and is refreshed within the TTL window.

**What you need to do:**

* **No action required.** This is a transparent performance improvement. All existing API endpoints behave identically. Role assignment and grant mutations continue to return the same responses - the invalidation happens automatically after the mutation succeeds.
* **Suspensions and grant edits are now near-instant.** Previously, a role assignment deletion or grant change could take effect only after a session was re-opened. Now, the change is visible on the very next conversation turn in any active session for the affected workspace.

</details>

<details>

<summary>v0.9.360 - Platform API: External Role Conflict Detection and Deletion Improvements (July 2026)</summary>

#### External Role Conflict Detection and Deletion Improvements

External role creation and role grant creation now return proper conflict responses when a duplicate is detected, and external role deletion correctly removes associated grants before deleting the role.

**What changed:**

* **Reliable conflict detection on role creation.** Creating an external role with a `name` or `external_name` that already exists in the workspace now consistently returns `409 Conflict`. Previously, certain database driver configurations could cause this to surface as a `500` instead of a clean conflict response.
* **Reliable conflict detection on grant creation.** Creating a role grant that duplicates an existing active grant (same role, resource, and access level) now consistently returns `409 Conflict`. Similarly, supplying a `role_id` that does not belong to the workspace consistently returns `422 Unprocessable Entity`. Previously, these could intermittently surface as `500` errors.
* **External role deletion removes associated grants.** Deleting an external role now removes all role grants associated with that role in the same operation. Previously, roles that had been granted access to resources could not be deleted because the grant records blocked removal, resulting in a `500` error. Grants are meaningless without their parent role, so they are cleaned up automatically as part of the delete.

**What you need to do:**

* **No action required for most users.** If you have error-handling code that catches `500` errors on role or grant creation and treats them as duplicates, you can now rely on `409` consistently.
* **Role deletion is now safe for roles with grants.** You no longer need to manually remove grants before deleting an external role. The delete operation handles this automatically.

</details>

<details>

<summary>v0.9.359 - Platform API: Channel Access Request Endpoint (July 2026)</summary>

#### Channel Access Request Endpoint

Workspaces can now request access to internal-preview channels (email, SMS, iMessage, WhatsApp) through the API. These channels are not self-serve provisionable - they require operator-managed enablement due to regulatory requirements or managed rollout. The new endpoint records the request and notifies the Amigo operator team to action it.

**What changed:**

* **New `POST /channels/access-requests` endpoint.** Submit a request for operator enablement of an internal-preview channel. The endpoint accepts a `channel` field (one of `email`, `sms`, `imessage`, `whatsapp`) and an optional `note` field for context (use case, volume, timeline). Returns `202 Accepted` with `{"status": "received", "channel": "..."}`. This endpoint does not provision anything - it hands off to the operator team for out-of-band enablement.
* **Low-privilege permission.** The endpoint requires only `Channel.view` permission, since requesting enablement is informational and provisions nothing.
* **PHI safety.** The `note` field is delivered to an operator channel outside the tenant boundary. Do not include patient data (PHI) in the note.

**What you need to do:**

* **No action required for existing workflows.** This is a new endpoint. If you are building a console or portal that surfaces internal-preview channels, you can now offer a "Request access" action that calls this endpoint.
* **Ensure API keys carry `Channel.view`.** Any API key used to call this endpoint must have a role with the `Channel.view` permission.

</details>

<details>

<summary>v0.9.358 - Platform API: Per-Workspace Speech-to-Text Provider Setting (July 2026)</summary>

#### Per-Workspace Speech-to-Text Provider Setting

Workspaces can now select a preferred speech-to-text provider for caller audio through voice settings. The platform honors the preference when the chosen provider is available and compatible with the call's language, and falls back to the default routing chain otherwise.

**What changed:**

* **New `stt_provider` field on voice settings.** The voice settings request and response models now include an `stt_provider` field. Accepted values are `"deepgram"`, `"openai"`, and `"cartesia"`. Setting the field to `null` (the default) inherits the deployment-default speech-to-text routing.
* **Graceful fallback.** When the workspace's chosen provider cannot serve a call - because it does not support the caller's language or is temporarily unavailable - the platform falls through to the default provider chain automatically. A set-but-unusable preference never causes a transcription failure.
* **Operator audio unaffected.** Clinical-staff (operator) audio always uses the default provider regardless of workspace preference.

**What you need to do:**

* **No action required.** Existing workspaces default to `null` (inherit deployment default). To select a provider, update your workspace voice settings and set `stt_provider` to one of the accepted values.

</details>

<details>

<summary>v0.9.357 - Platform API: Trust Root Gate for Starting Conversations as External Principals (July 2026)</summary>

#### Trust Root Gate for Starting Conversations as External Principals

Starting a conversation anchored to an entity that has an active external role assignment now requires explicit permission and is audit-logged. This closes the trust root for the API-start path - workspace credentials can still anchor conversations to ordinary entities freely, but anchoring to a mapped external principal is treated as impersonation and gated accordingly.

**What changed:**

* **Permission gate on principal conversations.** When a workspace credential creates a conversation with an explicit entity ID and that entity has an active external role assignment, the platform now checks for the `Conversation.CreateForPrincipal` permission. This permission is granted to admin and owner roles only. Credentials without this permission receive a `403` response.
* **Audit logging.** When a principal session is opened through this path, the platform records a `principal.session.opened` audit event capturing the conversation ID, the acting credential, and the target entity ID.
* **No change for ordinary entities.** Conversations anchored to entities without an active role assignment are unaffected. No additional permission is required, and no additional audit event is emitted.
* **No change for sessions without a principal.** The normal patient conversation path (no entity ID or entity without a role assignment) behaves exactly as before.

**What you need to do:**

* **Verify credential roles.** If you use workspace API keys to start conversations anchored to entities with external role assignments, ensure those keys carry admin or owner roles. Member, operator, and viewer credentials will now receive `403` on these requests.
* **No action for standard conversations.** If you do not use external principals or do not anchor conversations to entities with role assignments, no changes are needed.

</details>

<details>

<summary>v0.9.356 - Platform API: External Principals - Entity-Anchored, Role-Scoped Tool Access (July 2026)</summary>

#### External Principals - Entity-Anchored, Role-Scoped Tool Access

The platform now supports external principals - a system for binding external user identities to workspace entities and scoping their agent sessions to specific tools, skills, and data based on assigned roles. This is designed for partner integrations where an end user (such as a clinician or care coordinator) drives conversations through the agent and should only access resources their role permits.

**What changed:**

* **Role registry.** Workspaces can now define internal roles with normalized names that map to external role strings from the customer's identity system. Roles are workspace-scoped and unique.
* **Role assignments.** An assignment record binds an external user (by subject key and optional subject ID) to a workspace entity with one or more internal roles. Assignments carry status (active, suspended, revoked), provisioning method (roster sync or just-in-time), and role verification state.
* **Role grants.** Grants map a registry role to a specific resource (integration endpoint, skill, or knowledge base scope) with read or write access. Grants are immutable for audit - changes insert a new active row and mark the prior one as superseded.
* **Session principal resolution.** When a session opens anchored to an entity with an active role assignment, the platform resolves a session principal carrying the user's identity, role union, allow-grants, and the workspace's restricted resource set. This happens once at session open.
* **Grant gating at tool dispatch.** Restricted resources (any resource referenced by any active grant in the workspace) require the principal's roles to grant the required access level. Unrestricted resources remain workspace-open. Workspaces with no grants are unaffected.
* **Identity bindings on custom token exchange.** Integration authors can now declare `identity_bindings` on custom token exchange authentication, mapping exchange parameters to the verified principal's identity attributes (`principal.subject_key` or `principal.subject_id`). Bound parameters are removed from the agent-facing tool schema, stripped from model-supplied parameters, and injected from the verified principal at dispatch. A missing principal value fails the call closed.
* **Param binding constraints on grants.** Grants can carry an optional parameter binding that injects a principal attribute into call parameters at dispatch, enabling self-scoping (for example, restricting a provider ID parameter to the principal's own subject ID).
* **Entity conflict detection on session mint.** The identity service now rejects session mint requests where the external subject key is already bound to a different entity, preventing silent identity rebinding through the session creation path.

**What you need to do:**

* **No action required for existing workspaces.** Workspaces that do not define external roles or grants are completely unaffected. All tools and resources remain workspace-open.
* **To adopt external principals:** Define internal roles in your workspace, create role assignments binding external users to entities, and author grants scoping roles to specific integration endpoints, skills, or knowledge base scopes.
* **Update custom token exchange integrations** if you want to bind exchange parameters to the session principal's identity. Add `identity_bindings` to your integration's auth configuration, mapping parameter names to `principal.subject_key` or `principal.subject_id`. Each bound parameter name must reference a parameter already declared on the exchange's headers or body fields.
* **Handle new error responses.** Session mint requests that attempt to rebind an external subject to a different entity now return a `400` error with `error: "invalid_request"` indicating the identity/entity conflict.

</details>

<details>

<summary>v0.9.355 - Platform API: Inline Malware Scanning for Intake Uploads (July 2026)</summary>

#### Inline Malware Scanning for Intake Uploads

The intake pipeline now supports inline malware scanning for all file uploads. When scanning is enabled, every uploaded file is scanned before it is written to storage. Infected files are rejected immediately and never persisted. The feature is config-gated and off by default - existing deployments are unaffected until scanning infrastructure is provisioned.

**What changed:**

* **Fail-closed scanning.** When scanning is enabled, every intake upload (both HMAC-authenticated and link-based) is held in memory, scanned, and written to storage only after a clean verdict. If the scanner detects malware, the upload is rejected with a `422` response and nothing is persisted. If the scanner is unreachable or returns an error, the upload is rejected with a `503` response so the client can retry.
* **New error responses.** Two new error conditions are surfaced on intake upload endpoints:
  * `422 Unprocessable Entity` - returned when malware is detected in the uploaded file. The response body indicates that the upload was rejected by the malware scan.
  * `503 Service Unavailable` - returned when the upload scanner is unreachable. This is a transient condition and the upload can be retried.
* **Scan status on upload records.** Upload records now carry a scan status that reflects whether the file was scanned. When scanning is enabled and the file passes, the status is `clean`. When scanning is not enabled, the status remains `skipped` (unchanged from previous behavior).
* **No change when scanning is not enabled.** When the scanning infrastructure is not provisioned (the default), the upload path behaves exactly as before. No new error codes are returned, and uploads are persisted with a `skipped` scan status.

**What you need to do:**

* **Handle new error codes.** If you submit files to the intake upload endpoints, update your error handling to account for `422` responses indicating malware rejection and `503` responses indicating scanner unavailability. The `503` case is transient and should be retried.
* **No action required if scanning is not enabled.** The feature is off by default. Existing integrations continue to work without changes until your deployment enables scanning.

</details>

<details>

<summary>v0.9.354 - Platform API: Intake Upload Filename Removed from Event Payload (July 2026)</summary>

#### Intake Upload Filename Removed from Event Payload

The intake pipeline no longer includes the filename in the `intake.file.received` event payload or entity state. Filename is treated as protected health information (PHI) and is now stored exclusively in the intake uploads ledger, which is workspace-scoped and access-controlled.

**What changed:**

* **Filename removed from event payload.** The `filename` field has been removed from the `intake.file.received` event data. The event still includes `customer_slug`, `content_type`, `sha256`, `size_bytes`, and `volume_path`.
* **Entity lookup reads from the intake ledger.** The intake uploads lookup tool now reads file metadata (filename, content type, size) from the intake uploads ledger rather than from entity state. All existing filters (filename search, MIME type, date range) continue to work. Date filters now use calendar-day granularity against the received timestamp.
* **No change to lookup response shape.** The fields returned by the intake uploads lookup (`entity_id`, `filename`, `mime_type`, `size_bytes`, `uploaded_at`) are unchanged. Only the backing data source has moved.

**What you need to do:**

* **No action required for lookup consumers.** If you query intake uploads through the agent's entity lookup tools, the response shape and filters are unchanged.
* **Update event payload consumers.** If you consume `intake.file.received` events directly and rely on the `filename` field, note that it is no longer present. File metadata including the filename is available through the intake uploads lookup tool.

</details>

<details>

<summary>v0.9.353 - Platform API: Outbound Calls Now Require Use Case ID (July 2026)</summary>

#### Outbound Calls Now Require Use Case ID

Outbound call creation now requires a `use_case_id`. The legacy `phone_from` field and direct caller-ID override path have been removed. All outbound calls are now routed through the channel manager, which selects the optimal outbound phone number bound to the specified use case.

**What changed:**

* **`use_case_id` is now required.** The `use_case_id` field on the create outbound call request is no longer optional. Every outbound call must specify which use case to route through. The channel manager selects the outbound phone number based on the use case binding.
* **`phone_from` removed.** The `phone_from` field has been removed from the create outbound call request. You can no longer specify a direct caller ID override. All caller-ID resolution flows through the channel manager via `use_case_id`.
* **Mutual exclusion validator removed.** The previous validation that required exactly one of `phone_from` or `use_case_id` has been removed since `use_case_id` is now the only accepted field.
* **Legacy phone number management endpoints removed.** The phone number CRUD endpoints on the agent engine (`POST /phone-numbers`, `GET /phone-numbers`, `GET /phone-numbers/by-number`, `GET /phone-numbers/{id}`, `PATCH /phone-numbers/{id}`, `DELETE /phone-numbers/{id}`) have been removed. Phone number management is handled entirely through the channel manager.
* **Legacy inbound webhook removed.** The legacy `POST /twilio-webhook` endpoint (without a use case ID path parameter) has been removed. Inbound call routing now uses the use-case-scoped webhook path exclusively.
* **`platform.phone_numbers` no longer queryable.** The `platform.phone_numbers` table has been removed from the data query registry. If you were querying this table through the data query API, those queries will no longer work.

**What you need to do:**

* **Update outbound call requests.** If you are creating outbound calls with `phone_from`, switch to `use_case_id`. Create a channel use case with the appropriate phone number binding, then pass that use case ID when initiating outbound calls.
* **Stop using legacy phone number endpoints.** If you were managing phone numbers through the agent engine API, migrate to the channel manager use case and setup endpoints.
* **Update inbound webhook URLs.** If your telephony provider is configured to send inbound calls to the legacy webhook URL (without a use case ID), update the webhook URL to include the use case ID path parameter.
* **Update data queries.** If you have data queries referencing `platform.phone_numbers`, remove or replace them. Phone number data is managed through channel use cases.

</details>

<details>

<summary>v0.9.352 - Platform API: Inbound SMS and iMessage Now Drive Agent Turns Automatically (July 2026)</summary>

#### Inbound SMS and iMessage Now Drive Agent Turns Automatically

Inbound SMS and iMessage messages now automatically trigger an agent turn when they are received. Previously, inbound messages were persisted but required separate orchestration to start an agent turn. The platform now drives the turn as part of the same inbound processing, so the agent responds without additional coordination.

**What changed:**

* **SMS inbound messages drive agent turns.** When an inbound SMS is received and persisted, the platform immediately drives an agent turn for the associated use case. The sender's phone number and message body are forwarded to the agent. If the turn cannot be driven (for example, due to a transient error), the inbound message is not persisted - the upstream provider retries delivery, and the next attempt re-runs cleanly. This guarantees that every persisted inbound SMS has a corresponding agent turn.
* **iMessage inbound messages drive agent turns.** When an inbound iMessage is received, the platform persists the message (including any media attachments), then drives an agent turn for the associated use case. Text content and media references are forwarded to the agent. If the turn cannot be driven, both the message row and any stored media are rolled back, so the upstream provider retries delivery cleanly. Duplicate deliveries are detected before media is re-fetched or the turn is re-driven - a duplicate is acknowledged with no side effects.
* **Idempotency for iMessage improved.** Duplicate iMessage deliveries are now detected with an early check before any media fetch or turn processing. Previously, deduplication relied on a database constraint at insert time. The new approach avoids redundant media downloads and turn attempts on redeliveries.

**What you need to do:**

* **No action required.** If you have active SMS or iMessage use cases, inbound messages now produce agent responses automatically. No configuration changes are needed.

</details>

<details>

<summary>v0.9.350 - Platform API: Patient Memory Layer Removed (July 2026)</summary>

#### Patient Memory Layer Removed

The legacy patient memory layer has been removed from the Platform API. All memory-related endpoints and settings endpoints have been retired.

**What changed:**

* **Memory endpoints removed.** The following endpoints are no longer available:
  * `GET /v1/{workspace_id}/memory/analytics` - workspace-level memory analytics
  * `GET /v1/{workspace_id}/memory/{entity_id}/dimensions` - entity dimension scores
  * `GET /v1/{workspace_id}/memory/{entity_id}/facts` - entity memory facts
* **Memory settings endpoints removed.** The following endpoints are no longer available:
  * `GET /v1/{workspace_id}/settings/memory` - get memory dimension settings
  * `PUT /v1/{workspace_id}/settings/memory` - update memory dimension settings
* **Memory dimensions no longer configurable.** The six built-in memory dimensions (clinical, behavioral, operational, social, engagement, risk) and support for custom dimensions have been removed. Workspace settings no longer accept a memory configuration block.
* **Data access tools updated.** The platform's data query tools and agent runtime no longer reference patient memory tables or memory-related functions. Agent context that previously included memory snapshot and dimension summary functions has been updated accordingly.

**What you need to do:**

* **Remove any integrations that call memory endpoints.** If your application calls any of the memory or memory settings endpoints listed above, remove those calls. These endpoints now return errors.
* **Remove memory settings configuration.** If you manage workspace settings programmatically and include a memory configuration block, remove it. The platform ignores memory settings.
* **Update any agent function references.** If your agent configurations reference memory-related platform functions (such as patient memory snapshot or patient dimension summary), remove those references. These functions are no longer available.

</details>

<details>

<summary>v0.9.349 - Platform API: Email Channel Production Hardening (July 2026)</summary>

#### Email Channel Production Hardening

The email channel now includes production hardening for graceful shutdown, backlog observability, and dead-letter recovery.

**What changed:**

* **Graceful drain on shutdown.** During a rolling deployment or scaling event, email processing stops accepting new inbound turns and allows in-flight work to finish within a bounded drain window. Work that has not started remains eligible for another worker. This eliminates the previous behavior where routine shutdown could cancel a turn mid-processing.
* **Backlog depth gauges.** The platform now periodically emits two operational gauges: the count of queued turn wake signals and the count of sessions with in-progress or queued work. A rising value in either gauge indicates that inbound turns are arriving faster than they are being processed - a backlog condition that per-turn liveness heartbeats alone cannot detect.
* **Dead-letter redrive tooling.** Turns that exhaust their retry budget are dead-lettered with a durable event record carrying the full routing context (service, use case, session, caller, subject, and message references). An operator recovery tool can read this record, reconstruct a fresh processing item with a clean retry budget, release the original ingest claim, and re-enqueue the turn. The tool defaults to dry-run mode and requires an explicit flag to apply changes.
* **Enriched dead-letter event payload.** The dead-letter event now carries additional routing fields (service, use case, caller reference, subject, and parent outbound reference), making the event self-describing and enabling the redrive tool to reconstruct the turn without external lookups.
* **Front-door drop counters.** The ingest endpoint now emits counters when the idempotency claim or the work-list enqueue fails at the front door. These drops were previously invisible because they occur before the turn reaches the consumer's own metric series.

**What you need to do:**

* **No action required.** These are internal reliability improvements. If you operate email channel workloads, the new backlog gauges and dead-letter redrive capability provide additional operational visibility and recovery options. No changes to request format, response format, or API surface.

</details>

<details>

<summary>v0.9.348 - Platform API: Entity Resolver Serving-Layer Cutover (July 2026)</summary>

#### Entity Resolver Serving-Layer Cutover

The entity resolve endpoint (`POST /v1/{workspace_id}/world/entities/resolve`) now reads from the platform's optimized serving projections instead of the legacy entity store. The response contract is unchanged, but three summary fields are narrowed to fixed defaults and the external-ID resolution path now uses point reads against the canonical entity map.

**What changed:**

* **Serving-layer cutover.** Entity resolution now reads from the platform's deduplicated serving projections. The canonical\_id and external\_id identifier arms resolve through the canonical entity map with bounded point reads before the main entity lookup, replacing the previous scan-based approach. All other identifier arms (phone, email, entity\_id) continue to match against indexed entity columns.
* **External-ID resolution uses canonical key forms.** External-ID lookups now probe the canonical entity map using structured key forms derived from the system and value (`{system}:Patient:{value}`, `{system}:Practitioner:{value}`, `{system}:Organization:{value}`, `{system}:Appointment:{value}`, plus the bare value). Legacy external-ID aliases that were never minted as canonical keys are no longer resolvable.
* **Phone matching simplified.** The phone identifier arm now matches against the typed phone column only. The previous fallback to demographic data within the entity state has been removed. In production, all phone values were already projected into the typed column, so this change is lossless.
* **Summary field narrowing (wire-compatible).** Three fields on `EntityResolveSummary` are now fixed defaults:
  * `external_ids` is always `{}` - external aliases are managed through the canonical entity map, not carried on the entity row.
  * `tags` is always `[]` - the serving projection does not include tags.
  * `canonical_id` returns the canonical-map value when the entity was resolved through the map, otherwise the entity's MRN.
* **All existing fields and defaults are preserved.** The response schema is unchanged. Clients that consume `external_ids`, `tags`, or `canonical_id` will see the documented default values rather than populated data.

**What you need to do:**

* **Review any logic that depends on `external_ids` in resolve responses.** If you inspect `summary.external_ids` from resolve matches, note that it now always returns an empty object. External aliases are still used for resolution (via the canonical entity map), but they are not surfaced in the summary.
* **Review any logic that depends on `tags` in resolve responses.** The `summary.tags` field now always returns an empty list.
* **Update external-ID resolution expectations.** External-ID lookups now resolve through structured canonical key forms. If you have external aliases that were stored only in the legacy format and never minted as canonical keys, they will no longer resolve. Contact support if you encounter missing resolutions.
* **No changes to request format.** The `EntityResolveRequest` body is unchanged. All identifier fields (phone, email, canonical\_id, external\_id, entity\_id) continue to work as before.

</details>

<details>

<summary>v0.9.347 - Platform API: Durable Conversation Turn Counts (July 2026)</summary>

#### Durable Conversation Turn Counts

Text conversations now persist a turn count on the durable conversation record. Each inbound user message increments the count, providing accurate activity tracking for conversation lifecycle management and list ordering.

**What changed:**

* **Turn count persisted on the conversation record.** Every time the agent processes an inbound user message in a text conversation, the platform writes the updated turn count to the durable conversation record. The write is monotonic - the stored count can only increase, so signal retries or session resumptions cannot regress it.
* **Activity touch on every turn.** Each turn count write also updates the conversation's last-activity timestamp, keeping conversation list ordering and active/dormant lifecycle tracking aligned with real conversation activity rather than session-level events alone.
* **Fail-open persistence.** If the durable turn count write fails (for example, due to a transient storage error), the conversation turn proceeds normally. The failure is logged and metered, but the user's message is still processed and the agent responds. Turn count accuracy may lag temporarily, but conversation flow is never interrupted by a persistence failure.
* **Accurate turn counting from session state.** When a session resumes from persisted state, the turn count is derived from the actual inbound user messages in the interaction log rather than the raw entry count. This prevents internal state entries (such as tool responses or state markers) from inflating the turn count.

**What you need to do:**

* **No action required.** This is an internal improvement to conversation activity tracking. If you consume conversation list or detail responses, the ordering and activity timestamps now reflect per-turn granularity rather than session-level events only.

</details>

<details>

<summary>v0.9.346 - Platform API: MCP Token-Workspace Binding Enforcement (July 2026)</summary>

#### MCP Token-Workspace Binding Enforcement

The MCP server now enforces that token-based credentials match the workspace specified in the request. Previously, a valid token issued for one workspace could be presented with a different workspace identifier, and the request would be accepted. The server now rejects any request where the token's workspace scope does not match the requested workspace.

**What changed:**

* **Workspace claim validation on MCP requests.** When the MCP server receives a token-authenticated request, it verifies that the workspace encoded in the token matches the `X-Workspace-Id` header. If they differ, the server returns `403 Forbidden` with a message indicating the token does not belong to the requested workspace.
* **Unscoped tokens are rejected.** Tokens that are not scoped to a specific workspace - such as bootstrap tokens that have not been activated - are rejected with `403 Forbidden` and a message indicating the token is not scoped to a workspace.
* **API-key authentication is unchanged.** This change applies only to token-based (JWT) authentication on the MCP server. API-key-based MCP requests continue to work as before.

**What you need to do:**

* **Ensure your MCP client sends the correct workspace ID.** If you connect to the MCP server using token-based authentication, verify that the `X-Workspace-Id` header matches the workspace the token was issued for. Requests with a mismatched workspace ID that previously succeeded will now return 403.
* **No action needed for API-key authentication.** If you use API keys to authenticate MCP requests, no changes are required.

</details>

<details>

<summary>v0.9.345 - Platform API: Optional Sender Display-Name Alias on Email Use Cases (July 2026)</summary>

#### Optional Sender Display-Name Alias on Email Use Cases

Email channel use cases now support an optional sender display-name alias. When set, outbound emails render the From header as `Display Name <address>` instead of a bare address. This gives workspaces control over how the sender name appears to recipients without changing the verified sender address.

**What changed:**

* **New `sender_email_alias` field on email use cases.** The create use case request accepts a new `sender_email_alias` field (string, 4-40 characters, nullable). When provided, the alias is rendered as the display name in the From header of outbound emails. When `null`, the From header contains only the sender email address with no display name. The field is required-nullable - callers must include it in the request body with either a string value or an explicit `null`.
* **Unicode display names are supported.** The alias accepts any Unicode characters within the 4-40 character range. Non-ASCII names are encoded automatically in the outbound email headers per RFC 2047, so names like "Renee Cafe" render correctly in recipient mail clients.
* **Alias is immutable after creation.** Like `sender_email_address` and `email_type`, the alias cannot be changed after the use case is created. To use a different display name, create a new use case.
* **Alias appears in all use case responses.** The `sender_email_alias` field is included in create, get, and list use case responses. Existing use cases created before this change return `null` for the field.

**What you need to do:**

* **Update create use case requests.** If you create email use cases through the API, add `sender_email_alias` to your request body. Set it to a display name string (4-40 characters) or `null` if you want a bare sender address.
* **Handle the new field in responses.** If your code deserializes use case responses, account for the new `sender_email_alias` field (type: `string | null`).

</details>

<details>

<summary>v0.9.344 - Platform API: Audit Events Now Include Agent Engine Activity (July 2026)</summary>

#### Audit Events Now Include Agent Engine Activity

The audit events API now returns all audit activity, including events originating from the agent engine. Previously, only events written by the core platform were visible through the API. Agent engine audit events are now included in list, count, and aggregate responses.

**What changed:**

* **Agent engine audit events are now API-visible.** Audit event queries now return events from all platform components, including the agent engine. Previously, these events existed in the audit ledger but were filtered out of API responses.
* **`resource_type` is now nullable.** The `resource_type` field on audit event responses is now `string | null` instead of `string`. Agent engine events may not reference a specific resource, so this field can be `null`. The `service` and `action` fields remain non-nullable - all audit events carry both.
* **No changes to other response fields.** All other fields on the audit event response (`id`, `workspace_id`, `timestamp`, `service`, `actor_entity_id`, `actor_credential_id`, `action`, `resource_id`, `resource_name`, `ip_address`) are unchanged.

**What you need to do:**

* **Handle nullable `resource_type`.** If your code assumes `resource_type` is always a non-null string, update it to handle `null` values. This applies to any client that deserializes audit event responses or filters on `resource_type`.
* **Expect more audit events in responses.** List and count queries may return more results than before because agent engine events are now included. If you paginate through audit events, your total counts may increase.

</details>

<details>

<summary>v0.9.343 - Platform API: Workspace-Scoped Channel Resources (July 2026)</summary>

#### Workspace-Scoped Channel Resources

SES setups and channel use cases are now scoped to the workspace that created them. Previously, these resources were visible platform-wide to any caller with the appropriate permission. With this release, each resource is owned by its creating workspace, and all list, get, verify, and delete operations are restricted to the owning workspace.

**What changed:**

* **SES setups are workspace-owned.** When you create an SES setup, the platform records the creating workspace as the owner. List responses return only the setups your workspace owns. Get, verify, and delete requests for a setup owned by a different workspace return 404 - the response is indistinguishable from a nonexistent resource, so no information about other tenants' setups is leaked.
* **Channel use cases are workspace-owned.** The same ownership model applies to channel use cases. Creating a use case records your workspace as the owner. List, delete, and service binding operations are scoped to owned use cases only. Attempting to bind a service to a use case owned by another workspace returns 404.
* **Fail-closed tenancy.** If the ownership record cannot be stored after a successful create (for example, due to a transient error), the resource exists but is owned by no workspace. An unowned resource is invisible to all tenants and never appears in any workspace's list - it cannot leak across tenants. These orphaned resources are reconcilable by platform operators.
* **No changes to request or response schemas.** All SES setup and use case endpoints accept and return the same fields as before. The only behavioral change is the tenancy scoping described above.

**What you need to do:**

* **Review cross-workspace workflows.** If you previously relied on one workspace seeing SES setups or use cases created by another workspace, those resources are no longer visible. Each workspace must create its own setups and use cases.
* **No request changes required.** Existing API calls continue to work. The workspace is inferred from the authenticated context - no new parameters are needed.

</details>

<details>

<summary>v0.9.342 - Platform API: Durable Connector Outcome Events (July 2026)</summary>

#### Durable Connector Outcome Events

The connector runner now emits durable outcome events for both write-back attempts and sync runs, replacing the previous behavior where outcomes were tracked only in transient metrics and short-lived cache keys.

**What changed:**

* **Write-back outcome events.** Every write-back attempt to an external system now produces a durable outcome event recording whether the attempt completed or failed. On failure, the event includes a classification (permanent or transient) and a PHI-safe error code - vendor error messages that may contain patient data are never included. Per-sink isolation ensures that a failure writing to one external system does not prevent outcome recording for other systems.
* **Sync run outcome events.** Each connector sync run now emits a durable outcome event with status (success or error), record counts (fetched and emitted), and run duration. On failure, the same PHI-safe classification and error code are included. This makes the short-lived cache key a cache over a durable record rather than the only record of sync activity.
* **Permanent rejections now propagate.** Write-back handlers that previously swallowed permanent vendor rejections (HTTP 400/422) now propagate them so the outcome event is recorded. Per-sink isolation in the subscriber ensures other write targets still proceed.
* **Fail-open emission.** Outcome event emission is fail-open - a lost outcome event never breaks the write-back or sync path. Failures to emit are metered separately.

**What you need to do:**

* **No action required.** These are internal durability improvements. Connector behavior is unchanged from an integration perspective - write-backs remain at-most-once with no automatic retry. The durable outcome events provide better observability for connector operations.

</details>

<details>

<summary>v0.9.341 - Platform API: Email Use Case Inbox Posture Flags (July 2026)</summary>

#### Email Use Case Inbox Posture Flags

Creating an email channel use case now requires two additional fields that control unsubscribe behavior and inbound email acceptance. These fields were already enforced by the underlying channel infrastructure, but the public API was not passing them through, causing all email use case creation requests to fail.

**What changed:**

* **Two new required fields on email use case creation.** `unsubscribable` (boolean) and `accepts_cold_inbound` (boolean) are now required when creating an email use case. `unsubscribable` controls whether sends carry RFC 8058 unsubscribe headers and honor per-use-case unsubscribe lists. `accepts_cold_inbound` controls whether inbound email that does not reply to an outbound message is accepted after spam and authentication gates. Setting `accepts_cold_inbound` to `false` restricts the inbox to thread-only replies.
* **Marketing use cases must be unsubscribable.** If `email_type` is `marketing` and `unsubscribable` is `false`, the request is rejected with a validation error. Transactional use cases may set `unsubscribable` to `false` for must-send flows.
* **Both fields are returned on use case responses.** The use case object returned by create and list endpoints now includes `unsubscribable` and `accepts_cold_inbound` for email use cases. These fields are `null` for non-email channels (voice, iMessage).
* **Fixes email use case creation.** Previously, email use case creation through the public API failed because the required inbox posture fields were not forwarded to the channel infrastructure. This release fixes that path.

**What you need to do:**

* **Update email use case creation requests.** Add `unsubscribable` and `accepts_cold_inbound` to your email use case creation payloads. Both are required booleans. If you are creating marketing use cases, `unsubscribable` must be `true`.
* **Update response parsing if needed.** The use case response object now includes two new optional fields (`unsubscribable` and `accepts_cold_inbound`). If your integration strictly validates response shapes, update your model to accept these fields.

</details>

<details>

<summary>v0.9.340 - Platform API: Outbound Task ISO Validation and Dispatch Observability (July 2026)</summary>

#### Outbound Task ISO Validation and Dispatch Observability

Outbound task creation now validates scheduling timestamps at the boundary, and the dispatch pipeline surfaces more detail when post-call event emission fails.

**What changed:**

* **Scheduling timestamp validation.** The `scheduled_at` and `next_attempt_at` fields on outbound task creation are now validated as ISO-8601 datetimes before the task is accepted. Previously, a malformed value was passed through and projected as a null timestamp in the downstream scheduling query, causing the task to silently never dispatch. The task now fails immediately with an error identifying which field is invalid and the value that was rejected.
* **Improved dispatch failure observability.** When an outbound call is successfully placed but the post-call event cannot be emitted, the platform now emits a dedicated metric for claim-extension failures in addition to the existing dispatch-emission-failed metric. Log messages in this failure path now reference a reconciliation runbook for faster operator triage. These are internal observability improvements - no changes to API behavior or response schemas.
* **No changes to request or response schemas.** All outbound task endpoints accept and return the same fields as before. The only behavioral change is that previously-accepted malformed timestamps now return an error.

**What you need to do:**

* **Check your scheduling timestamp values.** If your integration passes `scheduled_at` or `next_attempt_at` values that are not valid ISO-8601 datetimes (for example, bare dates like `2026-07-15` without a time component, or freeform strings), those requests will now be rejected. Update your integration to send full ISO-8601 datetime strings (e.g. `2026-07-15T14:30:00Z`).
* **No action required for well-formed timestamps.** If your timestamps already include date, time, and timezone information, this change has no effect.

</details>

<details>

<summary>v0.9.339 - Platform API: Zoom Personal Meeting Link Resolution (July 2026)</summary>

#### Zoom Personal Meeting Link Resolution

The bot creation endpoint now resolves Zoom personal meeting links (vanity URLs) to standard meeting numbers automatically, so callers can pass a personal Zoom link and have the platform join the correct meeting.

**What changed:**

* **Personal meeting link resolution.** When a bot is created with a Zoom personal meeting link (e.g. `https://zoom.us/my/username`), the platform resolves it to a standard numeric meeting by following Zoom's redirect chain. Previously, personal links were rejected as invalid.
* **Expanded Zoom URL format support.** The meeting link parser now accepts additional Zoom URL path formats beyond the standard `/j/{meeting_number}` path, including web client paths. Vanity name validation ensures only well-formed personal link slugs are accepted.
* **New response field: `meetingNumber`.** The bot creation response now includes a `meetingNumber` field containing the resolved numeric meeting identifier. This is returned for all successful bot creations, whether the input was a direct meeting link or a personal link that was resolved.
* **New error detail for resolution failures.** If a personal meeting link cannot be resolved (for example, the vanity URL does not redirect to a valid meeting), the endpoint returns HTTP 400 with detail `personal_meeting_link_resolution_failed`. Direct meeting links that are malformed continue to return `invalid_meeting_link`.
* **Meeting link validation moved to the endpoint layer.** Meeting link parsing and meeting number validation now happen during request processing rather than during request body validation. This enables async resolution of personal links. The validation rules are unchanged - meeting numbers must be 9 to 12 digits.

**What you need to do:**

* **No action required for existing integrations.** Direct meeting links (`/j/{number}`) continue to work exactly as before.
* **Personal meeting links are now supported.** If you previously needed to resolve Zoom personal links before passing them to the API, you can now pass them directly.
* **Update response parsing if needed.** The bot creation response now includes a `meetingNumber` field. If your integration strictly validates response shapes, update your model to accept this new field.

</details>

<details>

<summary>v0.9.338 - Platform API: Events-Only Entity State (July 2026)</summary>

#### Events-Only Entity State

Entity state is now computed exclusively from events. The platform no longer writes entity records directly during entity creation or outbound task dispatch - all state is derived from the event stream and served from the downstream projection.

**What changed:**

* **Entity creation is events-only everywhere.** When the platform resolves or creates an entity (for example, during a call, patient registration, or appointment booking), it now always resolves the entity identifier from the serving projection or mints a deterministic identifier on a miss. The legacy direct-write path has been removed. This completes the migration to event-sourced entity state.
* **Outbound task dispatch no longer writes entity columns directly.** Outbound lifecycle transitions (scheduled, dispatched, failed, retried) are now recorded exclusively as events. The outbound projection computes the current status from these events. Previously, each lifecycle transition wrote status columns on the entity record in addition to emitting an event.
* **Stronger single-dispatch guarantees for outbound calls.** The platform now uses a fail-closed claim mechanism before placing an outbound call, with extended claim duration if the post-call event emission fails. This prevents double-dialing a patient when transient infrastructure issues occur. Previously, the single-dispatch guard relied on a conditional write that could be bypassed under race conditions.
* **Vendor identifier write-back removed.** When creating a patient in an external system, the platform previously wrote the external system's patient identifier back to the entity record. This write-back has been removed. If vendor identifier linking is reintroduced in a future release, it will flow through the event-sourced identity resolution pipeline rather than direct entity mutation.
* **No changes to request or response schemas.** All API endpoints return the same response shapes, field names, and data types as before. Entity resolution behavior is unchanged from the caller's perspective - entities are still deduplicated by canonical identifier when provided.

**What you need to do:**

* **No action required.** This is an internal state management change. All existing API calls, entity resolution behavior, and outbound task workflows continue to work without changes.
* **Outbound task consumers** may notice slightly different timing for status transitions in the projection, since state is now derived from events rather than written synchronously. The eventual consistency window is typically under a few seconds.

</details>

<details>

<summary>v0.9.337 - Platform API: Audit Event Visibility Filter for Legacy Parity (July 2026)</summary>

#### Audit Event Visibility Filter for Legacy Parity

Audit read endpoints now filter out rows that were never part of the original audit API contract, preventing errors when pages contain rows with missing required fields.

**What changed:**

* **Audit reads exclude non-platform-API audit rows.** All audit read operations - event listing, counts, and aggregate queries - now return only rows that originated from the platform API's audit writer. Previously, the analytical projection included additional event rows (such as voice agent engine events) that lack the `service` and `action` fields required by the audit response model. Pages containing these rows caused HTTP 500 errors because the response serializer expected non-nullable fields.
* **No changes to request or response schemas.** All audit endpoints return the same response shapes, field names, and data types as before. Pagination, filtering, and sorting behavior are unchanged.
* **Voice agent audit rows remain stored but are not API-visible.** The underlying data store retains all audit-domain rows. Rows that do not carry `service` and `action` values are excluded from API reads until a future release enriches them with the required fields and updates the response model.

**What you need to do:**

* **No action required.** If you previously encountered 500 errors on audit list or export endpoints, those errors are resolved. All existing API calls, filters, and pagination parameters continue to work without changes.

</details>

<details>

<summary>v0.9.336 - Platform API: Audit and Compliance Reads Migrated to Analytical Projection (July 2026)</summary>

#### Audit and Compliance Reads Migrated to Analytical Projection

Audit event queries and compliance reports now read from a high-performance analytical projection instead of the transactional database, improving query performance and aligning audit reads with the same data path used for analytics.

**What changed:**

* **Audit reads use the analytical projection.** All audit event queries - list events, PHI access reports, entity access logs, audit summaries, and audit exports - now read from the analytical event projection. This is the same projection that audit event writers have been emitting to since v0.9.310. The transactional audit table is no longer read by the platform API.
* **Compliance reports use audit service directly.** The HIPAA compliance report's audit statistics section now delegates to the same audit summary query used by the audit endpoints, eliminating a duplicate query path and ensuring consistent numbers between the audit dashboard and compliance reports.
* **No changes to request or response schemas.** All audit and compliance endpoints return the same response shapes, field names, and data types as before. Pagination, filtering, and sorting behavior are unchanged.
* **Improved query performance.** Time-range queries and workspace-scoped filters benefit from the analytical store's columnar layout and partitioning. Large audit exports (up to 50,000 rows) complete faster under typical workloads.
* **Service unavailability returns 503.** If the analytical data store is not configured or a query fails, audit endpoints return HTTP 503 with a static error message rather than silently returning empty results. This matches the error behavior of other analytics-backed endpoints.

**What you need to do:**

* **No action required.** All existing API calls, filters, and pagination parameters continue to work without changes. Response schemas are unchanged.
* **Update error handling if needed.** If your integration does not already handle 503 responses from audit endpoints, consider adding retry logic. A 503 indicates a transient infrastructure issue, not a permanent error.

</details>

<details>

<summary>v0.9.335 - Platform API: On-Demand Call Trace Analysis (July 2026)</summary>

#### On-Demand Call Trace Analysis

Call trace analysis now runs on demand when a client first requests it, replacing the previous scheduled batch approach. The first read for a call that has no analysis yet triggers the analysis pipeline automatically.

**What changed:**

* **Analysis triggered on first read.** When `GET /calls/{call_id}/trace-analysis` finds no analysis for a call that exists in the workspace, the platform triggers the analysis pipeline for that specific call. Previously, analysis was produced by a scheduled batch job, meaning callers had to wait for the next processing window.
* **Faster time to results.** Analysis is typically available within 2-5 minutes of the first request. Clients should poll the endpoint until the status transitions from `pending` to `ready`.
* **Updated status semantics.** The `pending` status now indicates that analysis has been started on demand rather than that it is waiting for a scheduled batch. `ready` and `unavailable` are unchanged.
* **Debounced triggers.** Repeated requests for the same call within a short window do not queue redundant analysis runs. The platform deduplicates triggers so that page refreshes and polling do not waste compute resources.
* **Simulation calls excluded.** Simulation calls do not trigger on-demand analysis, since they do not produce recordings that the analysis pipeline can process.
* **Fire-and-forget.** The trigger is strictly best-effort and never blocks or fails the read request. If the trigger cannot be dispatched (misconfiguration, transient infrastructure issue), the client sees `pending` and can retry later. A subsequent request after the debounce window will attempt the trigger again.

**What you need to do:**

* **Update polling expectations.** If your integration previously waited 30 minutes or more for analysis to appear, you can now poll more frequently. Analysis is typically ready within 2-5 minutes of the first request.
* **No endpoint changes.** The `GET /calls/{call_id}/trace-analysis` endpoint signature, response schema, and authentication are unchanged.

</details>

<details>

<summary>v0.9.334 - Platform API: Deterministic Secret Resolution for Connectors (July 2026)</summary>

#### Deterministic Secret Resolution for Connectors

Connector data source credentials now resolve using a deterministic-path-first strategy with automatic fallback to the existing configured path.

**What changed:**

* **Deterministic secret path resolution.** When the connector runner resolves credentials for a data source (base URL, username, password, client ID, or client secret), it first checks a deterministic path derived from the environment, workspace ID, and data source ID. If the secret exists at that path, it is used. If not, the runner falls back to the path stored in the data source's connection configuration. This dual-read approach ensures that existing data sources continue to work without changes.
* **Stable identity-based paths.** The deterministic path is keyed on the data source's stable identifier rather than its mutable name. Renaming a data source does not orphan its provisioned secrets.
* **No additional error noise.** The deterministic path probe does not log an error when the secret is not yet provisioned at the new path. This is the expected steady state until secrets are reprovisioned under the deterministic path. Transient failures still log normally.
* **Separate namespace.** Connector data source secrets use a dedicated namespace that is distinct from integration secrets, so the two surfaces do not interfere with each other.

**What you need to do:**

* **No action required.** Existing data sources continue to resolve credentials from their configured paths. When secrets are reprovisioned under the deterministic path (in a future update), the connector runner will automatically prefer the new path. No configuration changes are needed.

</details>

<details>

<summary>v0.9.333 - Platform API: Live Email Channel Turn Consumer (July 2026)</summary>

#### Live Email Channel Turn Consumer

Inbound email messages are now processed by a live consumer that drains the turn queue and drives agent turns automatically, completing the email channel's inbound processing pipeline.

**What changed:**

* **Inbound email turns are now processed live.** When an inbound email resolves to a workspace and session, the platform processes the agent turn and sends the eligible reply automatically. Previously, inbound resolution was available but the end-to-end reply path was incomplete.
* **At-least-once, exactly-one-turn processing.** Items are consumed peek-then-remove: a crash mid-turn leaves the item queued for redelivery. A completion marker prevents a re-delivered item from re-running a turn that already finished. A crash-recovery sweep re-wakes any session whose processing has stalled past the redelivery horizon. This ensures no inbound email is silently lost and no completed turn is duplicated.
* **Retry with dead-lettering.** Failed turns are re-enqueued with an incremented attempt counter. After the retry cap is reached, the item is dead-lettered with a durable event record. The ingest-time deduplication claim is preserved, so a later provider redrive cannot resurrect an abandoned turn.
* **Never re-send after delivery.** If the reply email was already accepted by the provider but the turn then fails (a persist error or late exception), the item completes with an error rather than retrying into a duplicate email. The narrow window between provider acceptance and completion marking is accepted until the channel service's send-side deduplication ledger lands.
* **Reply threading and unsubscribe compliance.** Reply emails thread back to the inbound message so the recipient sees a continuous conversation. For use cases that require unsubscribe compliance, the footer and headers are appended automatically. The agent's plain-text response is lowered to email HTML deterministically.
* **Unsubscribe field on channel use cases.** Channel use case records now carry an unsubscribable flag indicating whether the use case requires unsubscribe footer and one-click headers for compliance with bulk-sender rules. This flag is returned by the channel management service and consumed by the reply transport when constructing outbound emails.
* **Ships dark by default.** The consumer is disabled by default in all environments. It must be explicitly enabled per environment before inbound email turns are processed. A runtime kill switch allows disabling the consumer without a deploy.

**What you need to do:**

* **No action required for existing integrations.** The consumer ships disabled. Environments that want live inbound email processing must explicitly enable it. Existing email sending, use case configuration, and channel management APIs are unaffected.

</details>

<details>

<summary>v0.9.332 - Platform API: Refresh Token Grant No Longer Triggers IP Lockout (July 2026)</summary>

#### Refresh Token Grant No Longer Triggers IP Lockout

The refresh token grant type no longer participates in IP-based lockout. Previously, failed refresh token exchanges counted toward the shared IP lockout bucket, which could cause unrelated authentication flows from the same IP to be locked out.

**What changed:**

* **Refresh token failures no longer increment IP lockout counters.** Refresh tokens are high-entropy random values - guessing one is not a viable attack vector, so invalid or expired refresh tokens represent normal session expiry rather than credential brute force. The platform no longer counts these failures toward IP-based lockout.
* **Successful refresh token exchanges no longer clear IP lockout.** Since the refresh token grant no longer participates in the lockout system, successful exchanges do not reset lockout counters for the IP.
* **Prevents cross-grant lockout.** Previously, a single stale session retrying a dead refresh token could lock out every other authentication grant from the same IP. This was especially impactful when multiple services shared an IP, where one component's expired tokens could block all users from other authentication flows (such as OAuth login). This change eliminates that failure mode.
* **Token theft detection is unaffected.** Replay and theft of rotated refresh tokens continues to be handled by the existing token theft detection mechanism, independent of IP lockout.

**What you need to do:**

* **No action required.** This is a backward-compatible fix. Existing refresh token integrations continue to work as before, with improved reliability when sharing IPs with other authentication flows.

</details>

<details>

<summary>v0.9.331 - Platform API: Conversation Transcript Fallbacks (July 2026)</summary>

#### Conversation Transcript Fallbacks

Conversation and call detail endpoints now return transcript data more reliably when the primary turn-level history is unavailable.

**What changed:**

* **Voice call detail includes verified transcript fallback.** When a voice call has no turn-level conversation history but does have a verified post-call transcript, the call detail endpoint now returns that transcript as a single turn instead of an empty turns array. This ensures that call detail clients always display available transcript text rather than an empty message pane.
* **Voice conversation detail includes transcript fallback.** The voice conversation detail endpoint applies the same fallback - when no structured turns are available but a verified transcript exists, the response includes the transcript as a system-role turn with the call start timestamp.
* **Text conversation list includes accurate turn counts.** The conversation list endpoint now cross-references turn-level data when computing turn counts for text conversations. If the stored turn count is lower than the actual number of persisted turns, the response reflects the higher value. This ensures that conversation list views show accurate turn counts even when the summary record has not been updated.

**What you need to do:**

* **No action required.** These are backward-compatible improvements to data completeness. Existing integrations will see more complete transcript data in responses that previously returned empty turns or understated turn counts.

</details>

<details>

<summary>v0.9.330 - Platform API: User Model Feature Removed (July 2026)</summary>

#### User Model Feature Removed

The user model synthesis feature has been removed from the platform. This includes the agent tool for retrieving user models, the background emit bridge in the connector runner, and the underlying synthesis pipeline.

**What changed:**

* **`get_user_model` tool removed.** The `get_user_model` built-in tool is no longer available for agent configurations. This tool previously allowed agents to fetch a synthesized narrative summary of a patient during a conversation. It has been fully removed from the platform.
* **User model emit bridge removed.** The connector runner no longer includes a background loop for publishing synthesized user model updates. The associated environment-level toggle and feature flag are no longer recognized.
* **User model enrichment key removed.** The system-default `user_model` enrichment key is no longer automatically available on workspaces. Workspaces that had explicitly overridden this key with a custom enrichment key row are unaffected - their custom key remains, but the platform no longer provides a built-in default.
* **Synthesis pipeline removed.** The batch pipeline that generated holistic patient narratives from extracted memory facts has been decommissioned. No new user model syntheses will be produced.

**What you need to do:**

* **Remove references to `get_user_model`.** If your agent configurations or context graphs reference the `get_user_model` tool, remove those references. The tool will no longer be registered or callable.
* **Remove user model emit configuration.** If you previously enabled the user model emit bridge via the environment-level toggle, that setting is no longer recognized and can be removed from your connector runner configuration.
* **No data loss.** Previously synthesized user model data remains in your data stores but will no longer be updated or served through the platform.

</details>

<details>

<summary>v0.9.329 - Platform API: Semantic Search Tools and Data API Parameter Removed (July 2026)</summary>

#### Semantic Search Tools and Data API Parameter Removed

The deprecated semantic search tools and the `semantic` query parameter on the Data API have been removed.

**What changed:**

* **Semantic patient search and semantic event search tools removed.** The `semantic_patient_search` and `semantic_event_search` built-in tools are no longer available for agent configurations. These tools previously allowed natural-language similarity searches against patient and entity data. They have been fully removed from the platform.
* **`semantic` query parameter removed from Data API.** The `GET /data/{schema}/{table}` endpoint no longer accepts the `semantic` query parameter. Although the older contract accepted `semantic=<query>`, no registered Data API table exposed the required embedding column, so the branch did not produce similarity-ranked results. Sending the removed parameter now returns `400` rather than silently ignoring it.
* **`embedding` column no longer hidden.** The `embedding` column was previously excluded from Data API query results as a hidden column. It is no longer treated as hidden. This is a cleanup change with no practical effect since the underlying similarity search capability has been removed.

**What you need to do:**

* **Remove references to semantic search tools.** If your agent configurations reference `semantic_patient_search` or `semantic_event_search`, remove them. These tools will no longer be registered or callable. Use the standard `patient_lookup` tool for exact-match patient searches.
* **Stop using the `semantic` query parameter.** If you pass `semantic=` to Data API queries, remove it from your requests. The parameter is no longer supported.

</details>

<details>

<summary>v0.9.328 - [Removed] Connector Runner User Model Emit Toggle (July 2026)</summary>

#### Connector Runner User Model Emit Toggle

{% hint style="warning" %}
**Removed in v0.9.330.** The user model emit bridge and its environment-level toggle have been removed. This changelog entry is preserved for historical reference only.
{% endhint %}

~~The connector runner now supports an environment-level toggle for the user model emit bridge, providing a reliable fallback when the feature flag provider is unavailable.~~

</details>

<details>

<summary>v0.9.327 - Platform API: Markdown File Uploads in Customer Data Intake (July 2026)</summary>

#### Markdown File Uploads in Customer Data Intake

Customer data intake now accepts Markdown (`.md` and `.markdown`) file uploads alongside existing supported file types.

**What changed:**

* **Markdown uploads accepted.** Intake upload links now accept `text/markdown` as a valid content type. You can upload `.md` and `.markdown` files through the intake upload page or the API. Previously, Markdown files were rejected as an unsupported content type.
* **Browser content-type inference.** Browsers often report an empty content type for Markdown files. The upload page now infers `text/markdown` from the file extension when the browser does not provide a content type, so `.md` and `.markdown` files are handled correctly without manual intervention.
* **File picker includes Markdown.** The file picker on the intake upload page now includes `.md` and `.markdown` extensions, so Markdown files appear in the file browser by default.

**What you need to do:**

* **No action required.** If you use customer data intake, Markdown files are now accepted automatically. No changes to existing intake links or configurations are needed.

</details>

<details>

<summary>v0.9.326 - Platform API: Linear-Assigned Tasks Scoped to Investigation Only (July 2026)</summary>

#### Linear-Assigned Tasks Scoped to Investigation Only

When a Linear issue is assigned to Ami, the resulting task is now scoped to investigation only. Ami will identify the probable root cause and point to the specific code paths worth examining, but will not modify code or open a pull request.

**What changed:**

* **Investigation-only scope.** Tasks created from Linear-assigned issues now instruct the agent to investigate the linked error report, identify the probable root cause, and highlight the relevant code paths - without making code changes or opening pull requests. Previously, the task description asked the agent to fix the underlying bug and open a draft PR.
* **No change to the assignment flow.** The mechanism for assigning issues to Ami in Linear is unchanged. Only the scope of work described in the generated task is narrower.

**What you need to do:**

* **No action required.** Linear-assigned tasks will automatically use the new investigation-only scope. If your workflow previously relied on Ami opening draft PRs from Linear assignments, you will need to handle code changes separately after reviewing the investigation output.

</details>

<details>

<summary>v0.9.325 - Platform API: Provider Refresh Tokens (July 2026)</summary>

#### Provider Refresh Tokens

Provider-authenticated sessions now receive refresh tokens, bringing the provider login flow to parity with other authentication methods.

**What changed:**

* **Refresh tokens issued on provider login.** When a provider principal authenticates via Google OAuth, email OTP, magic link, or device code through a provider access grant, the token response now includes a `refresh_token`. Previously, provider logins returned only an access token and clients had to re-authenticate with a fresh credential each time the access token expired.
* **Provider refresh token rotation.** Provider refresh tokens follow the same rotation model as other refresh token sources - each use returns a new refresh token alongside the new access token, and the previous refresh token is invalidated. Replay of a consumed refresh token triggers theft detection.
* **Idle timeout respected.** Provider refresh tokens honor the workspace idle timeout configuration. If a provider refresh token is not used within the idle timeout window, it expires and the provider must re-authenticate.
* **Grant revalidation on refresh.** Each provider refresh verifies that the underlying provider access grant is still active and bound to the same provider entity. If the grant has been revoked or the entity association has changed, the refresh is rejected and the token is invalidated.
* **Workspace binding enforced.** Provider refresh tokens are bound to the workspace where the grant was issued. Attempting to use a provider refresh token against a different workspace returns an error.
* **DPoP support.** Provider refresh tokens work with DPoP sender-constrained tokens. When a provider login uses DPoP, the refresh token is DPoP-bound and subsequent refresh requests must present a valid DPoP proof.
* **Scope preservation.** Provider refresh tokens preserve the fixed provider scope set. Requesting different scopes during refresh is rejected.
* **Audit trail.** Provider refresh token issuance, rotation, and failures are recorded in the audit log with the provider principal type and grant identifier.

**What you need to do:**

* **Update provider login clients to store and use refresh tokens.** If your integration authenticates as a provider principal, you can now use the returned `refresh_token` to obtain new access tokens without re-authenticating. Previously, you had to obtain a fresh OTP, OAuth code, or magic link each time.
* **No breaking changes.** Existing provider login flows continue to work. The only difference is that the token response now includes a `refresh_token` field that was previously `null`.

</details>

<details>

<summary>v0.9.324 - Platform API: Approval Rejection Narrated on Same Turn (July 2026)</summary>

#### Approval Rejection Narrated on Same Turn

When a human reviewer declines a gated integration action, the agent now narrates the rejection on the same turn it consumes the decision, rather than echoing a stale "pending review" status and correcting itself on the next turn.

**What changed:**

* **Same-turn rejection narration.** Previously, when a reviewer rejected a gated action, the agent could echo the earlier "awaiting approval" status on the turn the rejection was consumed and only report the decline on the following turn. The agent now reports the rejection immediately, including the reviewer's reason if one was provided.
* **Grant path unchanged.** Approved actions already narrated the outcome on the same turn. This fix brings the rejection path to parity with the approval path.

**What you need to do:**

* **No action required.** This is a fix to agent narration behavior. Rejections are now reported to the user on the same turn the decision is consumed, with no change to API request or response shapes.

</details>

<details>

<summary>v0.9.323 - Platform API: Inbound Email Sender Authentication and Provenance (July 2026)</summary>

#### Inbound Email Sender Authentication and Provenance

Inbound email processing now enforces universal DMARC authentication and records richer sender provenance on every received message.

**What changed:**

* **Universal DMARC gate.** All inbound email - both threaded replies and cold inbound - now requires DMARC to pass before the message is accepted. Previously, DMARC was checked only on the cold inbound path. DMARC aligns SPF and DKIM verification to the sender's `From:` identity, so a passing result means the sender address is authentic. Messages that do not pass DMARC are silently dropped. This prevents spoofed senders from injecting messages into existing conversation threads via forged reply headers.
* **Threaded reply sender authorization.** On the threaded path (replies to platform-sent outbound email), the authenticated `From:` address must match the recipient the platform originally mailed. A reply from a different sender - even if DMARC-authenticated - is not accepted on the threaded path. This stops an unrelated but authenticated sender from riding a forged reply header into someone else's conversation thread.
* **Updated `from_address` semantics.** The `from_address` field on inbound email records now contains the authenticated `From:` header identity rather than the envelope return path. On threaded replies, this is the address that matched the outbound recipient. On cold inbound, this is the first `From:` header address.
* **Removed `from_header` field.** The `from_header` field has been removed from the inbound email response. The `from_address` field now always carries the authenticated sender identity, making `from_header` redundant.
* **New provenance fields.** Two new fields are available on inbound email records: `send_from_address` (the envelope return path, which is what SPF authenticated) and `from_headers` (the full list of `From:` header addresses as parsed by the mail infrastructure). These fields provide complete sender provenance without requiring access to the raw message.

**What you need to do:**

* **If you read `from_header`,** switch to using `from_address`, which now always contains the authenticated sender identity. The `from_header` field is no longer returned.
* **If you rely on `from_address` being the envelope sender,** note that it now contains the `From:` header identity instead. Use `send_from_address` if you need the envelope return path.
* **No action required for threaded reply handling.** The sender authorization is applied automatically by the platform. Replies from unauthorized senders are silently dropped.
* **No action required for DMARC.** The universal DMARC gate is applied automatically. Legitimate senders with properly configured DMARC records are unaffected.

</details>

<details>

<summary>v0.9.322 - Platform API: Static Tool Schema Validation on Skill Writes (July 2026)</summary>

#### Static Tool Schema Validation on Skill Writes

Skill create and update endpoints now validate the `input_schema` of each static tool against the same tool-calling subset enforced on the skill's own `input_schema`.

**What changed:**

* **Static tool schema validation at write time.** Each static tool attached to a skill is wrapped as a companion sub-tool, so its `input_schema` is subject to the same constraints as the skill-level schema. A top-level `anyOf`/`oneOf`, a non-object root type, or an `array` property missing `items` in any static tool's `input_schema` now returns a 422 error on create or update. Previously, only the skill-level `input_schema` was validated, and a non-conforming static tool schema could silently prevent the model from calling that tool.
* **Read path unchanged.** Pre-existing skills with non-conforming static tool schemas continue to load without error on GET and LIST. The validation is applied only on write requests, so historical data remains accessible.

**What you need to do:**

* **Review static tool schemas.** If you have skills with static tools whose `input_schema` uses top-level composition keywords (`anyOf`, `oneOf`, `allOf`, `$ref`), non-object root types, or array properties without `items`, update those schemas before your next skill create or update call. Reads of existing skills are unaffected.
* **No action required if your static tool schemas already conform.** The supported subset is the same as for skill-level schemas: root must be a JSON object type, no top-level composition keywords, and every array property must include `items`.

</details>

<details>

<summary>v0.9.321 - Platform API: Per-Channel Outbound Egress and Inbound Turn Routing (July 2026)</summary>

#### Per-Channel Outbound Egress and Inbound Turn Routing

Outbound message delivery now uses per-channel validation and send paths, and inbound channel messages are routed to agent sessions through a new platform-owned turn carrier with built-in idempotency and session threading.

**What changed:**

* **Per-channel outbound content validation.** The platform now validates outbound content against each channel's real capability envelope before sending. Email enforces subject (up to 998 characters), body (up to 1,000,000 characters), and attachment limits (up to 5 attachments, each up to 5 MiB). SMS enforces a 1,600-character message limit with no attachments. iMessage enforces up to 18,996 characters and at most one media attachment (up to 20 MiB). Content that violates these limits is rejected at construction time - before the irreversible provider send - rather than failing at the channel service.
* **Per-channel delivery tools.** Each messaging channel (email, SMS, iMessage) now exposes a dedicated agent tool (`send_email`, `send_sms`, `send_imessage`). The recipient and channel routing are supplied by the platform from authorized session state - the model supplies only the message content, closing the prompt-injection exfiltration vector. Voice and web channels do not have transactional egress tools.
* **Email template tool.** Email sessions with a configured template catalog expose a `send_email_template` tool that lets the agent select from pre-approved templates by name and supply template parameters. The platform validates parameters against the template's declared variables at send time. Templates are scoped to the use case - a template not owned by the current use case is rejected.
* **Automatic unsubscribe footer.** For use cases that require unsubscribe compliance, the platform automatically appends an unsubscribe footer to the agent-authored email body. The footer carries the required opt-out links, which the channel service resolves at send time. The agent cannot omit or alter the footer.
* **Reply-to threading on email sends.** The `send_email` endpoint now accepts an optional `reply_to_inbound_email_id` parameter to thread a reply into the inbound message's chain.
* **Inbound turn routing.** The platform resolves the workspace and service from the use case binding, deduplicates redelivered webhooks, and matches a reply to the session that sent the original outbound message. When no parent can be resolved, the platform starts a new session.
* **Session-level reply routing.** The platform records which session produced each outbound message. When a reply arrives, the platform resolves the parent reference back to that session, ensuring reply continuity. This routing is platform-owned - the channel service provides the parent-outbound fact but never decides which session an inbound belongs to.
* **Inbound authentication gate.** Inbound messages that the channel service could not authenticate are recorded as events but do not drive agent turns, preventing unauthenticated senders from triggering stateful actions.
* **New channel manager client methods.** `send_sms`, `send_imessage`, `send_email_template`, and `list_email_templates` methods are now available for per-channel outbound delivery.

**What you need to do:**

* **No action required for existing integrations.** Existing email send behavior is unchanged. The new per-channel tools are registered automatically when a session has channel routing context.
* **To use email templates,** configure templates on your email use case. The agent will see a `send_email_template` tool with the available template catalog.
* **Inbound turn routing** is handled automatically by the platform. No configuration changes are needed for reply threading or session resolution.

</details>

<details>

<summary>v0.9.320 - Platform API: Global User Model World Tool (July 2026)</summary>

#### Global User Model World Tool

A new built-in world tool lets the agent fetch the synthesized global user model for a patient on demand, and a new system-default enrichment key ensures user model data is available in every workspace without per-workspace configuration.

**What changed:**

* **New `get_user_model` world tool.** Agents can now call `get_user_model` with a patient's `entity_id` (from a prior patient lookup) to retrieve the synthesized global user model - a holistic, continuously-updated narrative of who the person is, merged across all past sessions. The response includes the model text, confidence score, source, and last-updated timestamp. If no model has been synthesized yet for the patient, the tool returns `found: false` with a null model rather than an error.
* **Built-in `user_model` enrichment key.** The `user_model` enrichment key is now a system default available for every workspace. Workspaces do not need to create an enrichment key row to use the global user model - it is present automatically. A workspace can override the built-in key by defining its own `user_model` enrichment key; the workspace-specific definition always takes precedence.
* **Always-on context rendering.** The global user model is also rendered as always-on context for the agent through the standard enrichment rendering path, so the agent sees it automatically without needing to call the tool. The tool is useful when the agent needs to fetch the model explicitly - for example, after a patient lookup in a conversation where the model was not part of the initial context.

**What you need to do:**

* **No action required.** The `get_user_model` tool and built-in `user_model` enrichment key are available automatically. The tool returns `found: false` until the memory synthesis pipeline has produced a model for a given patient, so it ships safely with no dependencies on pipeline availability.
* **To use the tool,** ensure your agent has access to the patient's `entity_id` (typically from a `patient_lookup` call) and include `get_user_model` in your context graph's available tools.

</details>

<details>

<summary>v0.9.319 - Platform API: Description Field on Email Templates (July 2026)</summary>

#### Description Field on Email Templates

Email templates now support an optional `description` field for documenting the purpose or usage of each template.

**What changed:**

* **New `description` field on email templates.** The create, get, list, and update email template endpoints now include a `description` field. The field accepts a string up to 4096 characters (whitespace trimmed) or `null`.
* **Create template.** You can supply `description` when creating a new email template. The field is optional and defaults to `null` if omitted.
* **Update template.** You can set or clear the description on an existing template by passing a string value or `null`. Omitting the field from the update request leaves the description unchanged.
* **Get and list responses.** Both the single-template detail and the template list responses now include the `description` field.

**What you need to do:**

* **No action required.** The field is optional. Existing templates have a `null` description. Clients that do not send or read `description` are unaffected.
* **To add descriptions,** include the `description` field when creating or updating templates.

</details>

<details>

<summary>v0.9.318 - Platform API: Streaming Endpoint Routed Through Durable Actor Model (July 2026)</summary>

#### Streaming Endpoint Routed Through Durable Actor Model

The streaming text interaction endpoint now routes through the durable actor model when the live actor flag is enabled, matching the behavior already available on the REST and poll paths. Streaming responses currently deliver the full turn reply as a single message event rather than token-by-token, with token-level streaming planned as a follow-up.

**What changed:**

* **Streaming endpoint uses the durable actor path.** When the live actor flag is enabled for a workspace, streaming text interactions (`/internal/text-interact-stream`) on existing web conversations now produce signals into the durable actor pipeline rather than using the legacy request-scoped path. The endpoint validates the conversation, queues the user message, and elects a durable actor if one is not already running.
* **Degrade-to-batch delivery.** Under the durable actor model, the streaming response delivers the agent's reply as a single `message` event followed by a `done` event, rather than token-by-token streaming. This matches the batch turn semantics of the durable actor. Token-level streaming under the durable model is a planned follow-up.
* **New-conversation turns stay on legacy.** Streaming requests that create a new conversation (no conversation ID provided) continue to use the existing streaming path. Only message turns on existing conversations are routed through the durable actor model.
* **Non-live workspaces are unchanged.** Workspaces without the live actor flag continue to use the existing streaming behavior with no changes.
* **Improved cleanup on actor spawn failure.** When a durable actor fails to start, the cleanup logic now logs rollback failures rather than silently suppressing them, improving diagnosability of transient issues.

**What you need to do:**

* **No action required for most integrations.** If you consume the streaming endpoint and are subscribed to the workspace SSE stream, the transition is transparent. The streaming response format (`message` and `done` events) is unchanged.
* **Token-level streaming consumers** should be aware that under the durable actor model, the response currently arrives as a single message rather than incremental tokens. Token streaming under this model will be available in a future release.

</details>

<details>

<summary>v0.9.317 - Platform API: Live Poll Path for Durable Text Conversations (July 2026)</summary>

#### Live Poll Path for Durable Text Conversations

Poll requests on existing web text conversations now route through the durable actor model when the live actor flag is enabled. Previously, polls on live conversations fell through to the legacy request-scoped path. Now, a poll on a live conversation checks for queued backlog and, if present, ensures a durable actor is running to drain it - delivering completions over the workspace SSE stream rather than in the poll response body.

**What changed:**

* **Poll routed through the live path.** When the live actor flag is enabled for a workspace, poll requests (`poll=true`) on existing web text conversations now use the durable actor model. If queued signals exist and no actor is currently running, the poll elects and spawns one. Completions are delivered over the SSE outbox, not in the poll response.
* **Poll response is status-only.** The poll response under the live path returns a status (`active` or `idle`) and an empty messages array. Clients receive actual agent messages and tool results through the workspace SSE stream.
* **No change for non-live workspaces.** Workspaces without the live actor flag continue to use the existing poll behavior with no changes.
* **No change for message turns.** Message turns on existing conversations continue to use the same durable actor path introduced previously.

**What you need to do:**

* **No action required for most integrations.** If you are already subscribed to the workspace SSE stream for live message delivery, polls under the live path work automatically.
* **REST-only poll clients** should be aware that under the live path, poll responses no longer carry completions in the response body. Subscribe to the workspace SSE stream to receive agent messages and background results in real time.

</details>

<details>

<summary>v0.9.315 - Platform API: Live Agent Messages over Workspace SSE (July 2026)</summary>

#### Live Agent Messages over Workspace SSE

Agent-initiated text messages from durable conversations are now pushed live over the workspace SSE stream, so subscribed web clients can render agent messages immediately without polling or waiting for a user turn.

**What changed:**

* **New `text.agent_message` SSE event.** When the agent produces a message outside the normal request cycle during a durable web text conversation - for example, after a background completion or other asynchronous signal - the platform publishes a `text.agent_message` event on the workspace SSE stream. The event includes the conversation ID and the agent's message text.
* **Web channel only.** The live push is emitted for web text conversations only. Non-web channels (SMS, WhatsApp) deliver agent-initiated messages through their own provider transport.
* **No polling required for live clients.** Clients subscribed to the workspace SSE stream receive agent messages as they are produced. Match the `conversation_id` field to filter events for the active conversation.
* **Fail-open delivery.** If the SSE push fails for any reason, the agent message is still recorded in the conversation journal and available via conversation history. The push is additive - it does not replace existing delivery paths.

**What you need to do:**

* **No action required for existing integrations.** Existing conversation history and turn endpoints continue to work as before. This is an additive delivery channel.
* **To use live push,** subscribe to the workspace SSE stream and listen for `text.agent_message` events. Use the `conversation_id` field to associate the message with the correct conversation.

</details>

<details>

<summary>v0.9.314 - Platform API: Live Background Tool Results over Workspace SSE (July 2026)</summary>

#### Live Background Tool Results over Workspace SSE

Background tool completions are now pushed live over the workspace SSE stream the instant they finish, so subscribed web clients can render results immediately without polling.

**What changed:**

* **New `text.background_result` SSE event.** When a background tool completes during a web text conversation, the platform publishes a `text.background_result` event on the workspace SSE stream. The event includes the conversation ID, tool name, call ID (with a `bg:` prefix matching the tool-call log entry), success status, result text, execution depth, and optional duration.
* **Web channel only.** The live push is emitted for web text conversations only. Non-web channels (SMS, voice) continue to deliver background results through their own transport.
* **No polling required for live clients.** Clients subscribed to the workspace SSE stream receive background completions as they land. The existing poll-based drain (sending `poll=true` on the turn endpoint) remains available for REST-only clients and as a fallback.
* **Result size cap.** The result text carried in the SSE event is capped to keep the push payload bounded. The full result is always available through the conversation turn history.
* **Fail-open delivery.** If the SSE push fails for any reason, the background completion is still recorded in the conversation journal and available via poll or next-turn drain. The push is additive - it does not replace the existing delivery paths.

**What you need to do:**

* **No action required for existing integrations.** The poll-based drain and next-turn drain paths continue to work as before. This is an additive delivery channel.
* **To use live push,** subscribe to the workspace SSE stream and listen for `text.background_result` events. Match the `call_id` field against tool-call log entries to correlate the result with the original dispatch.

</details>

<details>

<summary>v0.9.313 - Platform API: Poll for Background Tool Results (July 2026)</summary>

#### Poll for Background Tool Results

The text conversation turn endpoint now supports a poll mode that lets REST clients check for completed background tool results without sending a user message.

**What changed:**

* **New `poll` query parameter on Create Turn.** Pass `poll=true` to drain any background tool results that have completed since the last turn. The agent reports the results in its response. If nothing is pending, the response returns `status: "idle"` with no agent message and no LLM round is consumed.
* **New `idle` status value.** The turn response `status` field can now return `idle` in addition to `active` and `completed`. An `idle` status means the poll found no pending background results - the conversation is still active but nothing new has completed. Keep polling.
* **Validation constraints.** A poll request must not include a user `message` (returns 422) and must not be combined with SSE streaming via `Accept: text/event-stream` (returns 422). These constraints prevent ambiguous requests.
* **Conflict on concurrent access.** If the conversation is already processing another turn when a poll arrives, the endpoint returns 409 Conflict. Clients should retry after a short delay.
* **Idle polls do not generate audit events.** A poll that returns `idle` (nothing was drained) does not produce a `conversation.turn` audit record. Polls that deliver an update audit like a normal turn.

**What you need to do:**

* **No action required for existing integrations.** The `poll` parameter defaults to `false`, so all existing turn requests behave identically.
* **To use polling,** send periodic `poll=true` requests (no more than once every few seconds per conversation) after dispatching a background skill. Stop polling when you receive a non-idle response or when the conversation completes.

</details>

<details>

<summary>v0.9.312 - Platform API: Background Tool Completions Surface as Tool Calls (July 2026)</summary>

#### Background Tool Completions Surface as Tool Calls

Background tool completions now appear as first-class tool-call entries in the turn's tool-call logs, so the result of a background tool (success or failure) is visible in the Developer Console's inline agent trace and tool-call history.

**What changed:**

* **Background completions produce tool-call entries.** Previously, when a background tool finished, its result was injected as a free-text system message in conversation history. The tool-call logs only showed a synthetic "running" placeholder from the original dispatch, and the completion (success or failure) was invisible in tool-call views. Background tool completions now produce a dedicated tool-call entry with a `bg:`-prefixed call ID, the tool name, result payload, success status, and execution duration. This applies to both the live streaming path and the REST request-scoped drain path.
* **Two-entry lifecycle (interim).** The dispatch placeholder ("running") and the completion entry appear as two separate entries in the tool-call logs. A future update will consolidate them into a single record. The `bg:` call ID prefix distinguishes completion entries from the dispatch placeholder.
* **Completion visible in playground.** The Developer Console's inline tool-call cards now show a done or failed card for background tool completions, instead of leaving the tool stuck at "running" indefinitely.
* **Input not yet threaded.** The completion entry carries an empty input object because the completion signal does not include the original arguments. The dispatch placeholder retains the original input. Threading the input through to the completion entry is part of the planned single-record consolidation.

**What you need to do:**

* **No action required.** This is a visibility improvement. Background tools behave identically from the agent's perspective. Tool-call logs and the playground now surface results that were previously hidden.

</details>

<details>

<summary>v0.9.311 - Platform API: Approved Writes Surface as Tool Calls (July 2026)</summary>

#### Approved Writes Surface as Tool Calls

When an operator approves a gated integration write, the fired write now appears as a first-class tool call in the turn's effects and tool-call logs - and the agent narrates the approval outcome explicitly.

**What changed:**

* **Approved writes produce tool-call entries.** Previously, an approved integration write executed out-of-band and only appeared as a system message in conversation history. It now produces a tool-call entry in the turn's tool-call logs with a dedicated call ID (prefixed to distinguish it from LLM-initiated calls), input parameters, result payload, success status, and execution duration. This means approved writes are visible in the Developer Console's inline agent trace, tool-call history, and audit trails alongside regular tool calls.
* **Explicit approval narration.** The agent now receives a specific narration of the approval outcome rather than a generic background-tool-completed message. On success, the agent is instructed to confirm that the action was approved and completed. On failure (the write was approved but the integration call failed), the agent is instructed to tell the user the action was approved but could not be completed, along with the error detail.
* **Rejection and expiry paths unchanged.** Rejected and expired approval decisions continue to behave as before - no write fires, and the agent is informed accordingly.
* **Metrics tags for success and failure.** Approval-granted metrics now carry a success/failure tag so you can distinguish writes that ran and succeeded from writes that ran but returned an error.

**What you need to do:**

* **No action required.** This is a visibility and narration improvement to the existing approval flow. Approve and reject endpoints behave identically from the caller's perspective. Tool-call logs and conversation history now contain richer data for approved writes.

</details>

<details>

<summary>v0.9.310 - Platform API: Transport-Agnostic Integration Approval Resume (July 2026)</summary>

#### Transport-Agnostic Integration Approval Resume

Integration write approval resume no longer depends on an active streaming connection. The operator's approve or reject decision is now durably recorded and consumed by the agent on the conversation's next turn in any transport - REST, streaming text, and SMS all resume reliably.

**What changed:**

* **Durable approval decisions.** When an operator approves or rejects a pending integration write via `POST /integrations/approvals/{conversation_id}/approve` or `POST /integrations/approvals/{conversation_id}/reject`, the decision is now durably persisted in addition to the existing workspace event. The agent consumes the decision on the conversation's next turn regardless of channel.
* **Transport-agnostic resume.** Previously, approval resume relied on the text-streaming event subscription - conversations on REST or SMS channels could miss the resume signal. The agent now checks for a pending decision on every turn in every transport, so approved writes fire and rejected writes are declined consistently across REST, streaming text, and SMS.
* **Exactly-once execution.** The decision is consumed atomically on first read, so the approved write fires exactly once even if multiple turns or transports poll concurrently.
* **Synchronous write execution on resume.** When the agent resumes an approved write, the integration call executes synchronously within the resuming turn so the outcome is available immediately. This is required for request-scoped transports (REST, SMS) that do not drain background results.
* **Tenant isolation.** The durable decision is scoped to the authenticated workspace, so a caller authorized for one workspace cannot plant a decision that another workspace's agent would consume - regardless of the conversation ID format.
* **SSE events retained as nudges.** The existing workspace SSE events (`integration.approval_granted`, `integration.approval_rejected`) are still published as immediate-resume hints for streaming clients. They are no longer the mechanism that drives resume - the durable decision is.
* **No changes to approve/reject endpoint signatures or permissions.** The request and response shapes, permission requirements (`ReviewQueue.review`), and audit logging are unchanged.

**What you need to do:**

* **No action required.** This is a reliability improvement to the existing approval flow. Approve and reject endpoints behave identically from the caller's perspective. Conversations on REST and SMS channels now resume correctly after approval without any client changes.

</details>

<details>

<summary>v0.9.309 - Platform API: Update External Integration Endpoint (July 2026)</summary>

#### Update External Integration Endpoint

External integrations now support partial updates through a dedicated PATCH endpoint, letting you modify integration metadata without recreating the resource.

**What changed:**

* **New `PATCH /v1/{workspace_id}/external-integrations/{integration_id}` endpoint.** Updates an external integration's `name`, `display_name`, and/or `description`. All fields are optional - include only the fields you want to change. At least one field must be provided. Pass `description: null` to clear the description.
* **Duplicate name detection.** If the updated name conflicts with another integration in the same workspace, the endpoint returns a 409 Conflict error.
* **Permission requirements.** The endpoint requires an admin or owner role with `ExternalIntegration.update` permission.
* **Audit trail.** Every update action is recorded in the workspace audit log with the fields that were changed, the acting credential, and integration details.
* **Returns the updated integration.** The response includes the full integration object with all current field values.

**What you need to do:**

* **No action required for existing integrations.** This is a new endpoint that adds update capability. Existing integrations and workflows are unaffected.
* **Update API clients** if you manage external integration metadata programmatically - you can now modify integration names, display names, and descriptions without deleting and recreating the integration.

</details>

<details>

<summary>v0.9.308 - Platform API: Delete External Integration Endpoint (July 2026)</summary>

#### Delete External Integration Endpoint

External integrations can now be deleted through a dedicated endpoint, soft-removing the integration and revoking all associated credentials in a single operation.

**What changed:**

* **New `DELETE /v1/{workspace_id}/external-integrations/{integration_id}` endpoint.** Soft-deletes an active external integration by marking it inactive and revokes any active client credentials associated with that integration. Both operations happen atomically within a single workspace-scoped transaction, so there is no window where credentials remain active after the integration is removed.
* **Permission requirements.** The endpoint requires an admin or owner role with both `ExternalIntegration.delete` and `ApiKey.delete` permissions.
* **Audit trail.** Every delete action is recorded in the workspace audit log with the acting credential and integration details.
* **Returns 204 No Content on success.** If the integration does not exist or is already inactive, the endpoint returns a 404 error.

**What you need to do:**

* **No action required for existing integrations.** This is a new endpoint that adds delete capability. Existing integrations are unaffected unless you explicitly delete them.
* **Update API clients** if you manage external integration lifecycle programmatically - you can now clean up integrations and their credentials in a single call instead of revoking credentials individually.

</details>

<details>

<summary>v0.9.307 - Platform API: Human-in-the-Loop Approval Gate for Integration Writes (July 2026)</summary>

#### Human-in-the-Loop Approval Gate for Integration Writes

Integrations now support a configurable approval gate that requires human approval before write-classified (or all) endpoint calls execute during a text conversation. An operator reviews the pending action and approves or rejects it before any data is written to the upstream system.

**What changed:**

* **New `approval_policy` field on integrations.** Integrations now accept an `approval_policy` when created or updated. The field accepts three values: `none` (default, no approval required), `writes` (POST, PUT, PATCH, and DELETE endpoint calls require approval), or `all` (every endpoint call requires approval, including reads). The field is returned on all integration read endpoints (get, list).
* **New approval endpoints.** Two new endpoints let operators resolve a pending approval:
  * `POST /integrations/approvals/{conversation_id}/approve` - approves the pending action, which then executes.
  * `POST /integrations/approvals/{conversation_id}/reject` - rejects the pending action with an optional reason. The action is not executed, and the reason is surfaced to the agent.
* **Conversation-scoped, one-at-a-time.** Only one integration endpoint call can be pending approval per conversation at a time. If the agent attempts a second gated call while one is already pending, the second call is rejected with an instruction to wait.
* **Permission model.** Approve and reject endpoints require the `ReviewQueue.review` permission (operator, member, admin, or owner roles).
* **Audit trail.** Every approve and reject action is recorded in the workspace audit log with the acting credential and conversation context.
* **Text and SMS only.** The approval gate is enforced on text and SMS conversation paths. Voice conversations do not yet support the approval gate.
* **Fail-closed safety.** If the approval infrastructure is unavailable, gated endpoint calls are refused rather than executed without approval.

**What you need to do:**

* **No action required for existing integrations.** The default `approval_policy` is `none`, which preserves the current behavior where all endpoint calls execute immediately.
* **To enable approval gates,** set `approval_policy` to `writes` or `all` when creating or updating an integration. Then use the new approve/reject endpoints (or a future operator console UI) to resolve pending approvals during conversations.
* **Update API clients** if you read integration objects - the `approval_policy` field is now included in all integration responses.

</details>

<details>

<summary>v0.9.306 - Platform API: External Consumer Subjects for External User Sessions (July 2026)</summary>

#### External Consumer Subjects for External User Sessions

External user sessions now use a subject-based identity model. Instead of requiring a pre-existing materialized world entity, external integrations provide a stable subject key that the platform resolves into a durable consumer subject. This decouples session identity from world entity materialization and supports both identified users and anonymous consumers.

**What changed:**

* **New `external_subject_key` and `subject_type` parameters on `external_user_session` grant.** When creating an external user session, you now provide an `external_subject_key` (a stable identifier for the end user, such as an email or account ID) and a `subject_type` (`user` or `anonymous`). The platform hashes the key and resolves or creates a durable consumer subject scoped to the integration and workspace. The `consumer_entity_id` parameter is now optional and, when provided, must reference an existing materialized world entity.
* **`consumer_subject_id` replaces `consumer_entity_id` as the token subject.** The JWT `sub` claim is now set to the consumer subject ID rather than a world entity ID. Tokens include new `consumer_subject_id` and `subject_type` claims. The `consumer_entity_id` claim is present only when a materialized entity was provided and verified.
* **Token response includes subject fields.** The token response now returns `consumer_subject_id`, `subject_type`, and optionally `consumer_entity_id` alongside the existing `session_id` field.
* **Subject-based conversation binding.** Text conversations created by external users are now bound to the consumer subject and service, not to a world entity. Conversation reads and closes for external users verify the subject binding, ensuring that an external user can only access conversations created through their own subject identity.
* **Only `external_integration` parents allowed.** The `external_user_session` grant now requires a parent token with `external_integration` principal type. Service account parents are no longer accepted for this grant type.
* **Stable subject resolution.** If the same `external_subject_key` is presented again for the same integration, workspace, and subject type, the platform resolves to the same consumer subject. This enables durable identity across sessions without requiring the external system to track platform-internal IDs.

**What you need to do:**

* **Update external user session creation requests.** Add the `external_subject_key` and `subject_type` parameters when requesting the `external_user_session` grant. The `external_subject_key` should be a stable identifier for the end user within your system.
* **Update token parsing.** If you read the `sub` claim or `consumer_entity_id` from external user tokens, update your code to use `consumer_subject_id` as the primary identity. The `consumer_entity_id` is now optional and only present when a materialized entity was provided.
* **Migrate from service account parents.** If you use service account credentials to create external user sessions, migrate to external integration credentials. Service account parents are no longer accepted for this grant type.

</details>

<details>

<summary>v0.9.305 - Platform API: On-the-Fly AI Metric Computation for Simulation Runs (July 2026)</summary>

#### On-the-Fly AI Metric Computation for Simulation Runs

Simulation runs now compute AI-evaluated metric values on the fly at run completion, so metric eval results are available immediately instead of waiting for the batch analytics pipeline.

**What changed:**

* **AI-evaluated metrics computed at run completion.** When a simulation run completes, the platform now computes values for all active AI-evaluated metrics in your workspace before the metric evaluation loop begins. Previously, these metrics relied solely on the batch analytics pipeline, which could take minutes to hours. Metric evals that reference AI-evaluated metrics now resolve to passed or failed immediately instead of remaining in a pending state.
* **Same prompt and parsing contract as the batch pipeline.** The on-the-fly computation uses the identical prompt construction and value parsing rules as the batch analytics pipeline. Numerical values are range-clamped, categorical values are normalized and validated against the configured category set, and boolean values are parsed from standard token patterns. This ensures that on-the-fly values match what the batch pipeline would produce for the same conversation.
* **Bounded concurrency.** AI metric computation for a run's sessions is parallelized with a concurrency cap to balance throughput against resource usage. Large runs with many sessions and metrics are processed efficiently without overwhelming the model endpoint.
* **Graceful failure handling.** If computation fails for a particular metric or session (for example, due to a transient model error), that metric is skipped and falls through to pending status. The rest of the run's metrics and evals continue without interruption.
* **Token usage metered.** Each model call for on-the-fly metric computation emits a usage event tagged as simulation traffic, so these costs appear in your usage analytics alongside other simulation LLM costs. The metering event includes the metric key for attribution.
* **No double-counting.** The batch pipeline's source scope excludes simulation conversations, so on-the-fly values and batch values are never produced for the same conversation. The two paths are independent producers with no cross-deduplication required.

**What you need to do:**

* **No action required.** On-the-fly computation is automatic for all workspaces with active AI-evaluated metrics. Metric evals on simulation runs will begin resolving immediately rather than remaining pending. If you have dashboards or automation that poll for metric availability, they may see results arrive faster than before.

</details>

<details>

<summary>v0.9.304 - Platform API: LLM Usage Metering for Greeting and Sim Recommend (July 2026)</summary>

#### LLM Usage Metering for Greeting and Sim Recommend

The platform now emits token usage events for two additional LLM call sites - greeting pre-generation and simulation recommendation requests - so all model invocations are reflected in billing and usage analytics.

**What changed:**

* **Greeting pre-generation metered.** When the agent pre-generates a greeting message at session creation time, the platform now emits a usage event reporting the prompt and completion tokens consumed. Previously, these tokens were not tracked, so greeting generation did not appear in usage analytics or billing.
* **Simulation recommend metered.** The simulation recommend endpoint, which generates candidate caller utterances from current conversation state, now emits a usage event for the tokens consumed. These events are tagged with a simulation traffic class, so they are billed and reported separately from production usage.
* **Fire-and-forget emission.** Usage events are emitted asynchronously and do not block the request path. If emission fails, the error is recorded in operational metrics but the calling request completes normally. Zero-token responses (where the model returns no usage data) are skipped automatically to avoid empty metering rows.
* **No change to existing metering.** Voice call token usage and per-turn text/simulation token usage continue to be emitted as before. This change adds coverage for call sites that were previously unmetered.

**What you need to do:**

* **No action required.** Usage events are emitted automatically. If you consume usage data from the billing or analytics pipeline, you will see new events for greeting pre-generation and simulation recommendation requests. Overall reported token consumption may increase slightly, reflecting tokens that were previously consumed but not tracked.

</details>

<details>

<summary>v0.9.303 - Platform API: External Integration Credentials (July 2026)</summary>

#### External Integration Credentials

The platform now supports external integration credentials - scoped client credentials that let third-party systems create external user sessions on behalf of your workspace, restricted to specific integrations and services.

**What changed:**

* **New credential management endpoints.** Four new endpoints under `/v1/{workspace_id}/external-integrations/{integration_id}/credentials` let you create, list, rotate, and revoke external integration credentials. Each credential is bound to an integration and a set of services, and can only create external user sessions for those services.
* **New `external_integration` principal type.** Credentials created through these endpoints authenticate as `external_integration` principals. These principals are exempt from IP allowlist checks and MFA enforcement, similar to service accounts.
* **External user sessions accept `external_integration` parents.** The external user session grant now accepts parent tokens with either `service_account` or `external_integration` principal type. External integration parents are restricted to creating sessions only for services in their credential's allowlist.
* **Credential rotation.** The rotate endpoint generates a new client secret and immediately invalidates the previous one. The credential ID and client ID remain unchanged.
* **Credential revocation.** The revoke endpoint permanently disables a credential. Revoked credentials cannot authenticate or create sessions.
* **Existing service account behavior unchanged.** Service accounts continue to work as before with no service-level restrictions on external user session creation.

**What you need to do:**

* **No action required for existing integrations.** Service account credentials continue to work as before. Use external integration credentials when you want to grant a third-party system scoped access to create external user sessions for specific services, without giving it full service account privileges.
* **Permissions required.** Creating, rotating, and revoking external integration credentials requires the Integration update, API key create, and API key delete permissions.

</details>

<details>

<summary>v0.9.302 - Platform API: Per-Turn Token Usage Metering for Text and Simulation Channels (July 2026)</summary>

#### Per-Turn Token Usage Metering for Text and Simulation Channels

The platform now emits per-turn token usage events for text sessions and simulation sessions, enabling accurate billing and analytics for all model invocations within a conversation turn.

**What changed:**

* **Token usage emitted for text and simulation turns.** Each agent turn now emits a usage event that reports the total prompt and completion tokens consumed during that turn. These events feed into the billing pipeline and are available in usage analytics.
* **Multi-round tool loops report summed tokens.** When a turn involves multiple model invocations (for example, an initial response followed by one or more tool calls and follow-up responses), the reported token counts are summed across all rounds in the loop. Previously, only the last round's tokens were reported, which under-counted usage for multi-step turns.
* **Timeout and error rounds included.** If a model invocation times out or errors after partially consuming tokens, those tokens are still included in the summed total. This prevents under-billing when a turn fails partway through a multi-round loop.
* **No change to voice channel metering.** Voice calls already emit token usage through the existing voice pipeline. This change brings text and simulation channels to parity.

**What you need to do:**

* **No action required.** Token usage events are emitted automatically. If you consume usage data from the billing or analytics pipeline, you will see new events for text and simulation turns. Usage totals for multi-round turns may be higher than previously reported, reflecting the actual tokens consumed across all rounds.

</details>

<details>

<summary>v0.9.301 - Platform API: Incremental Sync for FHIR Connectors (July 2026)</summary>

#### Incremental Sync for FHIR Connectors

FHIR-based connectors now use incremental sync, fetching only resources that changed since the last successful poll instead of re-fetching the entire dataset each cycle.

**What changed:**

* **Incremental polling by default.** Each poll cycle records a sync cursor after successfully fetching a resource type. The next poll uses that cursor to request only resources updated since the previous sync, significantly reducing data transfer volume and poll duration for large FHIR stores.
* **At-least-once delivery preserved.** The cursor uses an inclusive boundary so resources updated at the exact cursor timestamp are re-fetched. The existing deduplication layer suppresses duplicate processing, so the overlap is harmless.
* **Automatic full re-fetch on idle.** The sync cursor expires automatically after an extended idle period. When a connector resumes after the cursor has expired, it performs a full re-fetch that re-seeds the deduplication cache. This is an intentional safety net - no data is missed when a connector is paused and resumed.
* **Fail-open on cursor errors.** If the cursor cannot be read or written due to a transient storage issue, the connector falls back to a full re-fetch for that cycle. A degraded cursor costs one extra full poll, never missed data.
* **Mid-fetch failure safety.** If a poll fails partway through pagination, the cursor is not advanced. The next poll retries the same time window, and deduplication makes the overlap idempotent.

**What you need to do:**

* **No action required.** Incremental sync is automatic for all FHIR-based connectors. Existing connectors will begin incremental polling on their next cycle. The first poll after this update performs a full fetch to seed the cursor; subsequent polls are incremental.

</details>

<details>

<summary>v0.9.300 - Platform API: Simulation Emotion Injection Gated by Voice Channel (July 2026)</summary>

#### Simulation Emotion Injection Gated by Voice Channel

Simulation sessions now apply injected emotion data only when the session is configured for voice modality, matching the behavior of production channels.

**What changed:**

* **Emotion injection limited to voice simulations.** Injected emotion signals (valence, arousal, emotion label) on a simulation step are now honored only when the session is running in voice mode. Text and web simulation sessions ignore injected emotion data and rely on text-based classification only, matching production behavior where acoustic emotion is not available.
* **Fixes empathy parity with production text.** Previously, injecting emotion data on a text simulation session could cause the agent to respond with elevated empathy levels that would never occur in a real production text conversation. The agent now behaves identically in simulation and production for each channel type.
* **No change to voice simulations.** Voice simulation sessions continue to accept and process injected emotion data as before. When voice simulation support is enabled for a session, emotion injection applies automatically.

**What you need to do:**

* **No action required for most users.** If your simulation steps do not inject emotion data, behavior is unchanged. If you inject emotion data on text simulations, those injections are now silently ignored rather than applied. Update your test scenarios to use voice-mode sessions if you need to test emotion-driven agent behavior.

</details>

<details>

<summary>v0.9.299 - Platform API: Outbound (Agent-First) Simulation Sessions (July 2026)</summary>

#### Outbound (Agent-First) Simulation Sessions

Simulation sessions now support an outbound conversation direction where the agent speaks first against task context, enabling testing of proactive outreach workflows.

**What changed:**

* **New `direction` parameter on session creation.** The create simulation session endpoint now accepts a `direction` field with values `inbound` (default) or `outbound`. Inbound keeps the existing behavior. Outbound makes the agent generate the opening message using task context derived from the bound entity, matching the production outbound call path.
* **Outbound opening uses production greeting path.** The agent's first message in an outbound simulation is generated through the same process used for production outbound voice calls, so the opening reflects real task context (appointment details, patient information) rather than a generic greeting.
* **Incompatible with lazy initialization.** Requesting `direction="outbound"` with `lazy=true` returns HTTP 422. Outbound requires the agent to produce an opening message at session creation time, while lazy defers initialization to the first caller step.
* **Entity ID provides task context.** When an `entity_id` is provided on an outbound session, it is used both for caller context resolution and for building the outbound task context. When omitted, a generic outbound greeting context is used.

**What you need to do:**

* **No action required for existing sessions.** The default direction is `inbound`, so all existing integrations behave identically.
* **To test outbound workflows:** Set `direction` to `outbound` on session creation and provide an `entity_id` for the target patient. The agent will generate a contextual opening message.

</details>

<details>

<summary>v0.9.298 - Platform API: Simulation Session Tool Loading Fix (July 2026)</summary>

#### Simulation Session Tool Loading Fix

Fixed an issue where integration tools and platform functions could fail to load on simulation session steps after the initial session creation.

**What changed:**

* **Workspace context now set on every simulation step.** Previously, the workspace context established during session creation did not persist to subsequent step requests. This caused workspace-scoped tools (integration endpoints and platform functions) to silently fail to load from the second turn onward, while workspace data query tools were unaffected because they scope themselves independently.
* **All tool types now load consistently.** Integration tools and platform functions now load correctly on every simulation step, matching the behavior of the first turn and production sessions.

**What you need to do:**

* **No action required.** This fix is automatic. If you observed missing tool calls or unexpected agent behavior on multi-turn simulation sessions, those issues should now be resolved.

</details>

<details>

<summary>v0.9.297 - Platform API: Tool Result Context Limit Raised to 128 KB (July 2026)</summary>

#### Tool Result Context Limit Raised to 128 KB

The agent engine now retains up to 128 KB of a tool's output in the agent's working context, up from the previous 5 KB limit. This means large tool outputs - such as extracted document text or detailed query results - reach downstream tools and skills with full fidelity instead of being truncated.

**What changed:**

* **Tool result context cap raised from 5 KB to 128 KB.** Tool outputs up to 128 KB are now passed intact through the agent's working context. Previously, outputs were truncated at 5 KB, which could silently discard data that downstream tools or skills needed.
* **Truncation still applies above 128 KB.** Tool outputs exceeding 128 KB are still truncated, and the truncation is indicated in interaction logs and session data.
* **Aggregate context growth unchanged.** The overall prompt token budget and auto-compaction behavior are unchanged. The token-budget guard continues to manage total context size independently of individual tool result sizes.

**What you need to do:**

* **No action required.** This change is fully backward-compatible. Tools that previously returned outputs under 5 KB see no difference. Tools that returned larger outputs now have their results passed through with higher fidelity.
* **Review tools with large outputs.** If you have tools that return large payloads (e.g. document extraction, database queries), their full output is now available to the agent and downstream skills. Verify that this additional context produces the expected behavior in your workflows.

</details>

<details>

<summary>v0.9.296 - Platform API: iMessage Line Provisioning Simplified (July 2026)</summary>

#### iMessage Line Provisioning Simplified

iMessage channel setup now provisions the phone line synchronously. The line is assigned immediately when the setup is created, removing the previous asynchronous fulfillment lifecycle.

**What changed:**

* **Synchronous line assignment.** Creating an iMessage setup now returns the assigned phone number immediately. There is no longer a pending state or fulfillment ticket - the line is ready to use as soon as the setup is created.
* **`phone_number` field is always populated.** The `phone_number` field on phone number objects is now always a string (never null). Lines no longer go through a pending state.
* **`line_request_id` field removed.** The `line_request_id` field has been removed from phone number objects. This field previously tracked the provider's fulfillment ticket, which is no longer applicable.
* **Add phone number endpoint removed.** The `POST /v1/sendblue-setup/{setup_id}/phone-number` endpoint has been removed. Lines are provisioned exclusively through the setup creation flow.
* **Phone number status values simplified.** The `status` query parameter on the list phone numbers endpoint now accepts two values: `assigned` and `blocked`. The `pending` status has been removed. Default filtering returns both statuses.
* **Assignment no longer rejects pending lines.** The assign phone number endpoint previously returned a `409` error when attempting to assign a pending line. Since lines are no longer pending, this validation has been removed. Blocked lines still cannot be assigned.
* **Setup response shows all non-blocked lines.** The setup GET response `phone_numbers` array previously filtered to lines that were both fulfilled and not blocked. It now includes all non-blocked lines (which is equivalent, since all lines are now fulfilled at creation).
* **Webhook changes.** The `line_assigned` webhook type is no longer registered during setup. The `line_blocked` webhook continues to operate as before.

**What you need to do:**

* **Update clients that check for null `phone_number`.** The field is now always populated. Remove null checks or pending-state handling.
* **Remove references to `line_request_id`.** This field is no longer returned in phone number objects.
* **Remove calls to the add phone number endpoint.** `POST /v1/sendblue-setup/{setup_id}/phone-number` no longer exists and will return a 404.
* **Update `status` filter usage.** If you pass `status=pending` to the list phone numbers endpoint, remove it. Only `assigned` and `blocked` are valid values.
* **No action needed for existing setups.** Previously provisioned lines with assigned numbers continue to work without changes.

</details>

<details>

<summary>v0.9.295 - Platform API: External User Authentication for Conversations (July 2026)</summary>

#### External User Authentication for Conversations

Conversation endpoints now support authentication by external end-users using short-lived JWTs, enabling client-side conversation access without exposing workspace API keys.

**What changed:**

* **External user JWT authentication.** The create conversation, get conversation, create turn, stream turn, and close conversation endpoints now accept external user JWTs in addition to API keys. External user tokens are scoped to a specific service and consumer entity, and each endpoint enforces that the conversation belongs to the authenticated user and matches the token's service.
* **Scoped permissions.** Each conversation operation requires a specific scope in the external user JWT: `conversations:create` for creating conversations, `conversations:read_own` for reading conversation details, `conversations:turn` for sending messages, and `conversations:close_own` for closing conversations. Requests without the required scope receive a 403 response.
* **Server-derived entity ID on create.** When an external user creates a conversation, the `entity_id` is derived from the token. Passing an explicit `entity_id` in the request body returns a 400 error.
* **Service binding enforcement.** The `service_id` on create must match the service bound to the external user token. Accessing a conversation that belongs to a different service returns a 403 error.
* **List conversations not available.** External users cannot list all workspace conversations. The list endpoint returns 403 for external user tokens.
* **Tool call access restricted.** External users cannot use the `include_tool_calls` query parameter on get conversation or create turn endpoints. Streamed turn responses filter out tool call events (`tool_call_started`, `tool_call_completed`) for external users.
* **Per-session rate limiting.** External user requests are rate-limited per session rather than per API key, so one user's activity does not affect rate limits for other users sharing the same parent credential.
* **Audit logging.** All external user conversation actions (create, turn, stream, close) are logged with actor identity, session, credential, and service metadata.

**What you need to do:**

* **To use external user auth:** Issue short-lived JWTs from your backend scoped to the target service and consumer entity, with the appropriate conversation permission scopes. Pass the JWT as a Bearer token in the `Authorization` header on conversation endpoints.
* **No action required for existing integrations.** API key authentication continues to work identically. All existing behavior is unchanged.

</details>

<details>

<summary>v0.9.294 - Platform API: iMessage Channel Support and Channel Kind Enum Update (July 2026)</summary>

#### iMessage Channel Support and Channel Kind Enum Update

The platform now recognizes `imessage` as a channel kind across all conversation and channel event endpoints, and the channel kind field uses a consistent enumeration across the API surface.

**What changed:**

* **`imessage` channel kind.** The channel kind enumeration now includes `imessage` alongside the existing values (`voice`, `sms`, `whatsapp`, `web`, `email`). Conversations and channel events can reference the `imessage` channel.
* **Idle threshold for iMessage.** iMessage conversations use an idle timeout consistent with other asynchronous messaging channels. Conversations on this channel follow the same lifecycle derivation as other long-lived messaging channels.
* **Channel event endpoint accepts all channel kinds.** The internal channel event ingestion endpoint now accepts any valid channel kind rather than a restricted subset. Channel kinds without a dedicated event handler resolve gracefully with a skipped status rather than returning a validation error.
* **Consistent enumeration.** The channel kind field is now a single consistent enumeration across all endpoints. No behavioral change for existing integrations using previously supported values.

**What you need to do:**

* **No action required for existing integrations.** All previously valid channel kind values continue to work identically.
* **To use iMessage:** Pass `imessage` as the channel kind when creating conversations or referencing channel events on iMessage-based interactions.

</details>

<details>

<summary>v0.9.293 - Platform API: Tool Calls on Conversation Turns (July 2026)</summary>

#### Tool Calls on Conversation Turns

The GET conversation detail endpoint now supports returning tool call information on each agent turn, giving you full visibility into what tools the agent invoked and their results.

**What changed:**

* **`include_tool_calls` query parameter.** The GET conversation endpoint now accepts an optional `include_tool_calls` boolean query parameter (default `false`). When set to `true`, each agent turn in the response includes a `tool_calls` array with details about every tool invocation that occurred during that turn.
* **`tool_calls` array on turns.** Each entry in the `tool_calls` array includes the tool name, a unique call ID, input parameters (as an object), the result string, a `succeeded` boolean, `duration_ms` (wall-clock execution time in milliseconds, or null on legacy turns), and an `error_message` field (populated when the call failed, null otherwise).
* **Opt-in for payload size and privacy.** Tool call data is excluded by default to keep response payloads small and because tool inputs and outputs may contain sensitive information. This matches the existing opt-in behavior on the POST turns endpoint.
* **Text and voice conversations.** Tool call data is available on text conversation turns. Voice conversation turns use the same response schema but do not currently populate tool call data.

**What you need to do:**

* **To retrieve tool calls:** Add `?include_tool_calls=true` to your GET conversation detail request.
* **No action required for existing integrations.** The default behavior is unchanged - turns are returned without tool call data unless you opt in.

</details>

<details>

<summary>v0.9.292 - Platform API: Structured Output for Skill Failure Routing (July 2026)</summary>

#### Structured Output for Skill Failure Routing

Skills can now opt into structured output mode so the platform can deterministically detect soft failures and route them to failure delivery instead of treating them as successes.

**What changed:**

* **Structured output opt-in.** Skills that set `use_structured_output` to true and provide a `result_schema` with a top-level `status` field now constrain the model's final answer to a guaranteed-schema JSON envelope. The platform reads the `status` field (`success` or `failure`) to determine routing. A skill that honestly reports a soft failure is now routed to the configured failure delivery path instead of being treated as a successful result.
* **Schema validation gates.** Structured output is only enabled when all gates pass: the skill opts in, the result schema declares a `status` field, the schema conforms to the strict JSON-schema subset (no numeric/string bounds, `additionalProperties: false` on all objects), and the model supports structured outputs. If any gate fails, the skill falls back to free-text output with no change in behavior - the request is never sent in a way that would cause an error.
* **Failure routing from structured results.** When a structured-output skill returns `status: failure`, the platform treats the result as a failed tool call and routes it through failure delivery. Previously, a skill that softly failed but narrated the failure as prose would be treated as a success because there was no machine-readable failure signal.
* **Free-text skills unchanged.** Skills that do not opt into structured output continue to work exactly as before. There is no heuristic that guesses failure from prose responses.
* **Error envelope detection unchanged.** The existing error-envelope detection (JSON results with a top-level `error` key) continues to work for atomic tools and non-structured results. Structured-output status takes precedence when present.

**What you need to do:**

* **To use structured output:** Set `use_structured_output` to true on your skill and provide a `result_schema` whose top-level object includes a `status` property (with values `success` or `failure`). Ensure the schema uses `additionalProperties: false` on all object definitions and avoids numeric/string constraint keywords.
* **No action required for existing skills.** If you do not set `use_structured_output`, behavior is unchanged.

</details>

<details>

<summary>v0.9.291 - Platform API: SMS Message List and Get Endpoints (July 2026)</summary>

#### SMS Message List and Get Endpoints

The SMS channel now supports listing and retrieving individual messages, giving you full visibility into both inbound and outbound SMS traffic.

**What changed:**

* **List SMS messages.** A new GET endpoint on the SMS resource returns inbound and outbound messages interleaved chronologically. Filter by use case, direction (inbound or outbound), peer phone number, and creation time range. At least one of use case or peer phone number is required. Results are paginated with an opaque continuation token and support sorting by creation time (ascending or descending, defaulting to newest first). The maximum page size is 200.
* **Get SMS message.** A new GET endpoint retrieves a single SMS message by ID. The response includes direction-specific fields - outbound messages include delivery status, handoff and delivery timestamps, error details, and segment count; inbound messages include sender, recipient, body, and segment count.
* **Send operation ID updated.** The send SMS operation ID changed from `send-sms` to `send-sms-message` for consistency with the new endpoints.

**What you need to do:**

* **Update any references to the `send-sms` operation ID.** If your integration references the operation ID (for example, in generated SDK clients), update it to `send-sms-message`.
* **No other action required.** The new list and get endpoints are additive.

</details>

<details>

<summary>v0.9.290 - Platform API: Authorization and Tenant Isolation on Data Endpoints (July 2026)</summary>

#### Authorization and Tenant Isolation on Data Endpoints

All data-plane endpoints - FHIR resources, patient search, insights, dashboards, and MCP tools - now enforce role-based authorization and workspace-scoped tenant isolation.

**What changed:**

* **New Data permission tier.** A three-level permission model now gates access to clinical and analytical data surfaces:
  * **Data:View** - Read a workspace's own data (FHIR resources, patient search/timeline/summary, insights schema and sessions, dashboards). Granted to all roles including viewer.
  * **Data:Write** - Create or modify clinical data via FHIR import and FHIR resource create/update. Granted to member tier and above. Viewers and operators cannot create or modify patient records.
  * **Data:Query** - Execute supported read-only SQL through the Insights endpoint and MCP `sql_query` / `call_function` tools. Admin tier only.
* **Permission checks on all data endpoints.** Every FHIR read endpoint (search, get, history, status, patient search/timeline/summary, and all view endpoints) now requires Data:View. FHIR import and resource create/update endpoints require Data:Write. The insights SQL endpoint requires Data:Query. Dashboard execution requires Data:View. Callers whose API key role lacks the required permission receive a 403 response.
* **MCP tool authorization.** The MCP `sql_query` and `call_function` tools now check Data:Query before execution. Callers without admin-level access receive a structured error response indicating the required permission. MCP authentication now validates that the credential carries a recognized platform role - unrecognized roles are rejected at authentication time with a 403 rather than being evaluated downstream.
* **Workspace-scoped SQL guard.** All raw SQL execution - whether from the insights SQL endpoint, MCP tools, dashboard panels, or the insights agent's tool loop - now passes through an additional validation layer that restricts queries to the documented workspace data surface. Queries must reference only allowed tables and must scope multi-tenant tables to the caller's workspace. Queries that reference tables outside the allowed surface, omit workspace scoping, use write statements, or call external data-source functions are rejected with a descriptive error. This ensures that the broad grants held by the platform's analytics connection cannot be used to access other tenants' data or platform-internal tables.
* **SQL length cap.** Raw SQL submissions are capped to prevent oversized statements from consuming excessive processing time.

**What you need to do:**

* **Review API key roles.** If you have integrations using viewer or operator API keys that create or modify FHIR resources, those requests will now return 403. Upgrade those keys to member tier or above.
* **SQL integrations require admin keys.** If you have integrations that use the insights SQL endpoint or the MCP `sql_query` / `call_function` tools, those API keys must have the admin role. Lower-privilege keys will now receive 403 responses.
* **Update SQL queries if needed.** Raw SQL queries must reference only the documented workspace data surface (world model tables, platform summary tables, analytics tables, and customer data views) and must include workspace scoping on multi-tenant tables. Queries referencing undocumented tables or missing workspace filters will be rejected. Review any custom SQL queries used in dashboards, insights sessions, or MCP tool calls.
* **No action required for standard read workflows.** If your integrations use viewer or higher keys and only read FHIR data, patient records, insights sessions, or dashboards, the new Data:View permission is granted to all roles and behavior is unchanged.

</details>

<details>

<summary>v0.9.289 - Platform API: Skill Turn Limits and Narration Safety Net (July 2026)</summary>

#### Skill Turn Limits and Narration Safety Net

Skills now honor their configured maximum agent turn limit, and the platform detects when a skill narrates a tool call as text instead of invoking it.

**What changed:**

* **Per-skill turn budget enforced.** Each skill defines a maximum number of agent turns (tool-calling rounds) it is allowed to use. The platform now enforces this configured limit at runtime, bounded by the platform-wide safety ceiling. Previously, all skills used the platform-wide ceiling regardless of their individual configuration. Skills that configure a lower turn limit - for example, a single-purpose lookup skill set to two rounds - now stop at that limit instead of running up to the global maximum.
* **Narration detection safety net.** If a skill with tools produces a final text response that contains fabricated tool-call markup instead of actually invoking a tool, the platform now treats this as a failure. The response is replaced with an error indicating that no tool was executed. This prevents downstream systems from receiving text that looks like a tool invocation but was never actually dispatched. Skills without tools are not affected - their text output is never flagged, since they have no tools to invoke.
* **Skill test executor updated.** The skill test runner applies the same per-skill turn budget enforcement, so test results match production behavior.

**What you need to do:**

* **Review skill turn limits.** If you have skills with a `max_agent_turns` value lower than the platform default, those skills will now stop at their configured limit. Verify that your skills have appropriate turn budgets for their intended use cases.
* **No action required for most users.** If your skills use the default turn limit or do not exhibit narration behavior, this change has no visible effect.

</details>

<details>

<summary>v0.9.288 - Platform API: Workspace-Bound Device Code Approval (July 2026)</summary>

#### Workspace-Bound Device Code Approval

Device codes are now bound to a workspace at issuance, and the approval step enforces that the approver holds a session scoped to that workspace. This prevents cross-workspace token misuse and ensures that CLI tokens are always tied to the workspace they were requested for.

**What changed:**

* **Device codes carry a workspace binding.** When a client requests a device code and includes a workspace ID, the identity service records the target workspace on the code. Both operator-intent and provider-intent device codes support this binding.
* **Approval enforces workspace match.** When an approver submits an approval, the platform verifies that the approver's session is scoped to the same workspace the device code targets. If the session targets a different workspace, or if the approver has no workspace selected (bootstrap session), the approval is rejected with a `workspace_selection_required` error that includes the target `workspace_id`. The approver can then select the correct workspace and retry.
* **Deny is always allowed.** Rejecting a device code does not require a workspace-scoped session. Any authenticated user can deny a device code request they did not initiate.
* **Token exchange uses the pinned workspace.** When the CLI polls for a token after approval, the workspace bound at issuance is authoritative. If the polling client also sends a workspace ID, the pinned value takes precedence on any mismatch.
* **Backward compatibility.** Device codes issued without a workspace binding (from clients predating this change) continue to work. For operator-intent codes without a binding, bootstrap sessions are rejected as before, and workspace resolution falls back to membership lookup at token exchange.

**What you need to do:**

* **No action required for most users.** If you use Agent Forge v0.4.16 or later, your device code requests already include the workspace ID automatically.
* **CLI or SDK integrations that use the device code flow** should include the `workspace_id` parameter when requesting a device code. This is not yet a hard requirement, but will become mandatory in a future release.
* **Console integrations handling the approval page** should handle the `workspace_selection_required` error by redirecting the user to workspace selection for the `workspace_id` returned in the error response, then retrying approval.

</details>

<details>

<summary>v0.9.287 - Platform API: Billing Period Anchored to Event Time (July 2026)</summary>

#### Billing Period Anchored to Event Time

Billing periods are now anchored to when an event actually occurred rather than when it was ingested into the platform. This corrects billing period assignment for backfilled events, which previously landed in the operator's current billing month instead of the historical month when the event happened.

**What changed:**

* **Billing period uses event time.** Metering events now derive their billing period from the time the event originally occurred. For live traffic the two timestamps are effectively identical, so there is no visible change. For backfilled or late-arriving events, the billing period now reflects the actual event date rather than the processing date.
* **Dispute audit records updated.** Per-conversation billing detail used for dispute audits now reflects when a call happened, not when the backfilled event was recorded. This makes audit rows consistent with the corrected billing periods.
* **No change for current-month billing.** If you have not performed any historical backfills, billing totals are unchanged. The fix only affects events where the occurrence time and the recording time fall in different calendar months.

**What you need to do:**

* **No action required for most users.** Billing periods are computed automatically. If you have not backfilled historical events, your billing data is unaffected.
* **Review historical backfills.** If you previously backfilled events that were assigned to the wrong billing month, those events will now appear in the correct historical period. Contact support if you need a reconciliation of previously invoiced periods.

</details>

<details>

<summary>v0.9.286 - Platform API: Eval Summary Built-in Dashboard (July 2026)</summary>

#### Eval Summary Built-in Dashboard

A new built-in dashboard - **Eval Summary** - is now available in every workspace. It provides a workspace-level view of simulation and eval metric scores, sourced from the durable analytical store.

**What changed:**

* **New Eval Summary dashboard.** The platform now ships a pre-configured `eval-summary` dashboard alongside the existing Data Browser Insights and Metrics Overview dashboards. It appears automatically for all workspaces with no configuration required.
* **Four default panels.** The dashboard includes: a summary row (distinct eval metrics, total value points, scored events, average confidence, and latest computation timestamp), an average eval score leaderboard by metric (horizontal bar chart), a daily average eval score trend (time series), and a categorical eval outcome distribution (stacked bar chart).
* **Three interactive filters.** Users can scope the view by time window (7 days, 30 days, 90 days, or 1 year), metric source (simulation, production, or all), and metric type (numerical, categorical, boolean, or all). The default view shows 30 days of simulation-sourced metrics across all types.
* **Workspace-aggregate scope.** The dashboard currently displays workspace-level aggregate scores. Per-service breakdowns will be added in a future release when service-scoped metric data becomes available.
* **300-second auto-refresh.** The dashboard refreshes at a five-minute interval by default.

**What you need to do:**

* **No action required.** The Eval Summary dashboard is available automatically. Navigate to the analytics section of the Developer Console to view it. If your workspace has simulation or production eval metric data, it will appear in the dashboard panels immediately.

</details>

<details>

<summary>v0.9.285 - Platform API: External User Authentication Tokens (July 2026)</summary>

#### External User Authentication Tokens

The platform now supports a dedicated `external_user` principal type for end-users who interact through text conversation channels. External user tokens are scoped to a specific service and carry only conversation-level permissions, enabling you to embed conversation capabilities in customer-facing applications without exposing workspace credentials.

**What changed:**

* **New `external_user` principal type.** Tokens with `principal_type` set to `external_user` represent end-users accessing text conversation services. These tokens are bound to a specific workspace, service, and session.
* **Restricted conversation scopes.** External user tokens can only carry the following scopes: `conversations:create`, `conversations:turn`, `conversations:read_own`, and `conversations:close_own`. Any scope outside this set causes the token to be rejected at both issuance and verification.
* **Service-bound access.** Each external user token includes a `service_id` claim that binds it to a specific text conversation service. The `consumer_entity_id` claim identifies the external user and must equal the token's `sub` claim.
* **No workspace role.** External user tokens carry no `role` claim and are rejected by all role-based workspace APIs, including agent engine endpoints and workspace management. This ensures end-user access is strictly limited to conversation operations.
* **Parent credential minting.** External user tokens are issued by a parent credential that holds the `external_user_sessions:create` scope. This scope is non-delegatable - it cannot flow into delegated session tokens.
* **New scopes added to the platform.** Five new scopes have been added: `conversations:create`, `conversations:turn`, `conversations:read_own`, `conversations:close_own`, and `external_user_sessions:create`. The conversation scopes are exclusive to external user tokens. The minting scope is exclusive to parent credentials.
* **Validation at issuance and verification.** External user claim invariants (required UUID claims, scope restrictions, absence of `role` and `conversation_id` claims) are enforced both when tokens are created and when they are verified by downstream services.

**What you need to do:**

* **No action required for existing integrations.** This is an additive feature. Existing tokens, roles, and permissions are unaffected. The new scopes are not included in any standard workspace role - they are grant-specific.
* **To use external user tokens**, your parent credential must carry the `external_user_sessions:create` scope. Contact your workspace administrator to configure this.
* **Review any custom scope handling.** If you have custom code that enumerates or validates scopes, be aware of the five new scopes listed above.

</details>

<details>

<summary>v0.9.284 - Platform API: Simulation Metering for Billing (July 2026)</summary>

#### Simulation Metering for Billing

Simulation runs now emit metering events that feed into the billing pipeline. This enables usage-based billing for simulation activity, covering run completions, wall-clock compute time, evaluation results, and LLM token consumption during scenario generation, evaluation judging, and simulated caller turns.

**What changed:**

* **Run completion metering.** When a simulation run completes (successfully or with an error), the platform emits a `simulation_runs` metering event (1 per run) and a `simulation_seconds` metering event recording the wall-clock duration. Both meters carry metadata including the run ID, service ID, status, scenario count, and duration.
* **Evaluation result metering.** Each eval result (assertion or metric check) emits a `simulation_eval_results` metering event, so evaluation volume is tracked independently from run volume.
* **LLM token metering for simulation-side calls.** LLM calls made by the platform for scenario generation, target spec inference, evaluation judging, and simulated caller turns now emit `llm_input_tokens` and `llm_output_tokens` metering events tagged with `metering_source: simulation`. These are distinct from agent-engine LLM usage and allow simulation token costs to be tracked separately.
* **Idempotent event identifiers.** All simulation metering events use deterministic identifiers derived from the run, eval key, or session context. Re-emitting the same event (due to retries or historical backfills) updates the existing record rather than creating a duplicate. This prevents double-billing.
* **Emit endpoint accepts `event_id`.** The metering emit endpoint now accepts an optional `event_id` field. When provided, the event is stored with that identifier, making re-emission idempotent. If omitted, the server generates a random identifier as before.

**What you need to do:**

* **No action required for most users.** Metering events are emitted automatically and do not change simulation behavior, API responses, or eval results.
* **Billing pilot participants** will see simulation usage reflected in usage reports. Review your simulation volume if you are enrolled in the billing pilot.
* **Backfill available for historical runs.** If you need historical simulation usage reflected in billing reports, contact support to arrange a backfill of metering events for runs that completed before this release.

</details>

<details>

<summary>v0.9.283 - Platform API: Tool Call Execution Default Changed to Blocking (July 2026)</summary>

#### Tool Call Execution Default Changed to Blocking

The default execution mode for tool call specifications has been changed from `background` to `blocking`. This is a **critical fix** - the previous `background` default caused text and voice agents to stop replying after tool calls because the tool result never returned in-band on the request-scoped path.

**What changed:**

* **Default execution mode is now `blocking`.** Tool call specifications that do not explicitly set an execution mode now default to `blocking`, which awaits the tool result inline within the current turn. Previously, the default was `background`, which dispatched the tool as a background task and returned a synthetic acknowledgment instead of the real result.
* **`background` mode is strictly opt-in.** If you want a tool call to run in the background (the agent keeps the floor and the result arrives later), you must explicitly set `execution` to `background` in the tool call specification for that binding. This opt-in model prevents unintentional fire-and-forget behavior.
* **Fallback behavior for unbound tools updated.** When a tool is invoked that is not explicitly bound in the current state's tool call specifications, the platform now defaults to `blocking` execution. Previously, unbound tools defaulted to `background`, which caused the same silent failure on the text path.
* **Automatic migration for existing configurations.** A data migration strips any previously persisted `background` execution mode from tool bindings that were saved with the old default. This ensures that existing agent configurations behave correctly under the new default without requiring manual updates. Only bindings where `background` was baked in as an unintentional default are affected - explicitly configured `background` bindings are not modified.

**What you need to do:**

* **No action required for most workspaces.** All existing workspace configurations that did not explicitly set an execution mode were affected by the previous `background` default. This fix restores correct behavior - agents will now reply with tool results as expected. The platform has automatically cleaned up previously persisted configurations.
* **Review any explicit `background` bindings.** If you intentionally configured specific tool bindings with `execution: "background"`, those bindings are unaffected and will continue to use background dispatch. No changes are needed for explicit configurations.
* **Verify agent behavior after tool calls.** If you observed agents failing to reply after tool calls in text or voice sessions, this fix resolves that issue. No configuration changes are needed on your side.

</details>

<details>

<summary>v0.9.282 - Platform API: Permission Enforcement on Simulation Routes (July 2026)</summary>

#### Permission Enforcement on Simulation Routes

All simulation endpoints now enforce role-based permission checks. Previously, several simulation routes - including run management, bridge execution, benchmark runs, coverage graph queries, and session lifecycle operations - did not verify that the caller's role carried the required permission. Any authenticated API key with workspace access could invoke these endpoints regardless of its assigned role.

**What changed:**

* **Read operations require Service view permission.** Listing and retrieving simulation runs, observing sessions, querying coverage graphs and paths, listing sessions and turns, viewing benchmark results, retrieving session intelligence, and converting configuration to turn policy now require the Service view permission on the caller's role.
* **Write operations require Service update permission.** Creating and completing simulation runs, creating and stepping simulation sessions, forking sessions, scoring sessions, creating test conversations, stepping test conversations, recommending next steps, destroying sessions, deleting coverage graphs, running the simulation bridge, running benchmarks, running suites, running individual saved cases, and generating bridge plans now require the Service update permission on the caller's role.
* **Consistent 403 responses.** Callers whose role does not carry the required permission receive a 403 Forbidden response. Previously, these requests would have been processed regardless of role.

**What you need to do:**

* **Review API key roles used for simulation workflows.** If you have API keys scoped to restricted roles (such as viewer or operator) that invoke simulation endpoints, verify those roles carry the appropriate Service view or Service update permissions. Keys with admin or owner roles are unaffected.
* **Update automation that triggers simulation runs or benchmarks.** CI pipelines or scripts that create simulation runs, execute bridge requests, or trigger benchmark batches must authenticate with a role that includes the Service update permission.
* **No changes for read-only dashboards using admin keys.** If your integration already uses admin or owner credentials, no changes are required.

</details>

<details>

<summary>v0.9.281 - Platform API: Workspace Creation Restricted to Developer Grant (July 2026)</summary>

#### Workspace Creation Restricted to Developer Grant

Workspace creation now requires the `workspaces:write` scope, which is carried only by admin and owner credentials. Previously, any authenticated user - including members, operators, viewers, and newly verified users with bootstrap tokens - could create a workspace. This change restricts workspace creation to users who have been explicitly granted administrative access.

**What changed:**

* **`workspaces:write` scope required.** The create-workspace endpoint now checks for the `workspaces:write` scope on the caller's credentials. Only admin and owner roles carry this scope. Requests from member, operator, or viewer credentials receive a 403 response.
* **Bootstrap tokens no longer grant workspace creation.** Tokens issued to newly verified users who have no workspace membership carry member-level read scopes but do not include `workspaces:write`. These users can no longer self-serve create a workspace. An existing administrator must provision them with admin or owner access first.
* **403 response detail updated.** The 403 error message for the create-workspace endpoint now reads "Missing the workspaces:write scope (developer grant) or unsupported principal type" to clarify the required authorization.

**What you need to do:**

* **Review automation that creates workspaces.** If you have scripts or CI pipelines that create workspaces via the API, ensure they authenticate with admin or owner credentials. Member-scoped API keys will no longer work for workspace creation.
* **Provision new developers as admin or owner.** New team members who need to create workspaces must be granted admin or owner access by an existing administrator before they can do so.
* **No changes for existing workspaces.** This change affects workspace creation only. All other workspace operations (view, update, delete) retain their existing permission requirements.

</details>

<details>

<summary>v0.9.280 - Platform API: Ordered Conversation State Transitions (July 2026)</summary>

#### Ordered Conversation State Transitions

Conversation turns now expose the full ordered list of context-graph state transitions, not just a collapsed summary. Previously, when a turn triggered multiple state transitions, the API reported only the first source state and the final destination state. The new `state_transitions` array preserves every intermediate transition in order.

**What changed:**

* **New `state_transitions` field on conversation turns.** Each turn now includes a `state_transitions` array containing every context-graph state transition that occurred during that turn, in order. Each element has `from` and `to` fields identifying the source and destination states.
* **`state_transition` field deprecated.** The existing singular `state_transition` field remains available for backwards compatibility but is now deprecated. It continues to report the first source state and the final destination state when multiple transitions occur. Use `state_transitions` for the complete ordered list.
* **Session log and call intelligence enrichment.** Decision metadata emitted through the analytics pipeline now includes the full ordered transition list, so downstream analytics and metric evaluation have access to the complete state traversal history.

**What you need to do:**

* **Migrate to `state_transitions`.** If your integration reads the `state_transition` field on conversation turns, plan to switch to the `state_transitions` array. The deprecated field will be removed in a future release.
* **No breaking changes.** The deprecated field continues to work as before. Existing integrations are unaffected until the field is removed.

</details>

<details>

<summary>v0.9.279 - Platform API: Inbound SMS Receive and Webhook Consolidation (July 2026)</summary>

#### Inbound SMS Receive and Webhook Consolidation

The SMS channel now supports receiving inbound messages. When a message arrives on a phone number assigned to an SMS use case, the platform persists it and associates it with the correct use case automatically. Webhook routing has also been consolidated for simpler configuration.

**What changed:**

* **Inbound SMS message handling.** Messages sent to phone numbers assigned to an SMS use case are now received, validated, deduplicated, and stored automatically. Each inbound message is associated with the use case that owns the destination phone number, so replies and inbound traffic are tracked per use case.
* **Signature validation on inbound messages.** Every inbound SMS webhook delivery is validated against the sending account's credentials before processing. Requests with invalid or missing signatures are rejected. The platform handles protocol differences introduced by load balancer TLS termination transparently.
* **Deduplication.** If the same inbound message is delivered more than once (due to network retries), duplicate deliveries are silently acknowledged without creating duplicate records.
* **Messaging service inbound URL configuration.** When an SMS use case is created, the associated messaging service is automatically configured to forward inbound messages to the platform's inbound webhook. No manual webhook configuration is required.
* **Consolidated webhook routing.** All vendor webhook endpoints are now served under a single unified router. This is an internal routing change - all existing webhook URLs continue to work at the same paths with no changes required.

**What you need to do:**

* **For new SMS use cases**, inbound message handling is configured automatically at creation time. No additional setup is needed.
* **For existing SMS use cases**, you may need to update the messaging service's inbound URL to point to the platform's inbound webhook endpoint if you want to receive inbound messages. Contact support for migration assistance.
* **No action required for existing webhook integrations.** All webhook URLs remain unchanged.

</details>

<details>

<summary>v0.9.278 - Platform API: Voice Fire-and-Forget Parity for Background Tools (July 2026)</summary>

#### Voice Fire-and-Forget Parity for Background Tools

Background tools on the voice channel now have full parity with the text channel for non-blocking execution, cooperative cancellation, and write-tool durability. Previously, voice calls could dispatch background tools and query their status, but could not cancel in-flight tasks or guarantee that write-tool side effects survived call teardown.

**What changed:**

* **Cooperative cancellation on voice calls.** The agent can now request cancellation of an in-flight background task during a voice call, just as it can on the text channel. When a caller changes their mind, the agent invokes the built-in `cancel_background_task` tool with the task ID. The running task receives a cooperative cancellation signal at its next checkpoint and exits gracefully - this is a request, not a hard kill. The cancellation tool is offered automatically whenever background tasks are in flight.
* **Write-tool durability across call teardown.** Background tools classified as write tools (tools that persist data to the world model) now survive call teardown. If a voice call ends while a write tool is mid-execution, the tool runs to completion and emits its data event rather than being cancelled. Read-only tools are still cancelled immediately when the call ends. This prevents data loss for tools that are partway through committing a change when the caller hangs up.
* **Unified system tool set.** The cancellation tool and the existing task status query tool are now managed from a single registry shared by both the voice and text channels. This ensures consistent tool availability regardless of channel - if a background task is in flight, both tools are offered to the agent.

**What you need to do:**

* **No action required for existing configurations.** This is a behavioral improvement to how voice calls handle background tools. Existing tool definitions, context graphs, and conversation flows continue to work unchanged. The cancellation tool appears automatically when background tasks are running, and write-tool durability is applied based on your existing tool classifications.

</details>

<details>

<summary>v0.9.277 - Platform API: Degraded-Success Tier for Background Tools (July 2026)</summary>

#### Degraded-Success Tier for Background Tools

Background tools (non-blocking skills that run while the conversation continues) can now report a "degraded success" - a result that succeeded but with caveats, such as completing via a fallback after one or more sub-steps failed, or a step taking longer than expected. The agent treats the result as valid and does not retry, but can mention the caveats if the caller asks or if they materially affect the response.

**What changed:**

* **Soft-failure caveats on successful tool results.** A background tool that succeeds can now carry a list of degradation notes describing what went wrong during execution even though the final result is valid. For example, a skill that recovered after a failed sub-step reports "completed via a fallback after 1 failed sub-call(s)" alongside its successful result.
* **Agent steering for degraded successes.** When the agent receives a successful tool result with caveats, it treats the result as valid and does not lead with the caveats. It mentions them only if the caller asks, if they materially affect what the agent tells the caller, or naturally on the next turn. This applies to both voice and text channels.
* **Task status queries surface degradation notes.** When the agent checks the status of a completed background task (via the built-in task status tool), the response now includes any degradation notes alongside the result. This means the agent can explain caveats when the caller asks about a completed task.
* **Companion skill degradation derivation.** Multi-step skills (companion skills that orchestrate sub-tools) automatically derive degradation notes from their sub-tool execution log. If a sub-tool failed but the skill recovered and succeeded overall, or if a sub-step was unusually slow, the caveats are surfaced without any additional configuration.
* **Atomic tool degradation support.** Single-step tools can include degradation notes in their result envelope, enabling tools that succeeded from a cache, returned partial data, or hit a rate limit to communicate that context to the agent.

**What you need to do:**

* **No action required for existing configurations.** This is a behavioral improvement to how the agent handles and reports background tool successes that encountered issues along the way. There are no API contract changes. Existing integrations, tool definitions, and conversation flows continue to work unchanged. The degradation notes are used internally by the agent to produce more informed responses and are surfaced through task status queries.

</details>

<details>

<summary>v0.9.276 - Platform API: Outbound SMS Send and Delivery Tracking (July 2026)</summary>

#### Outbound SMS Send and Delivery Tracking

The SMS channel now supports sending outbound messages through a configured SMS use case, with automatic delivery status tracking via webhook callbacks.

**What changed:**

* **New send SMS endpoint.** `POST /v1/sms/` sends a single SMS message through the messaging service associated with an SMS use case. The sender is selected automatically from the use case's phone number pool based on configured routing policies. The response includes a message identifier, delivery status, and timestamps that update as the message progresses through the delivery lifecycle.
* **Pre-send compliance gating.** Before sending, the platform validates that the use case has at least one phone number in its sender pool and, for US long-code numbers, that a verified A2P 10DLC campaign is associated with the use case. This catches configuration issues synchronously with clear error messages instead of surfacing them as asynchronous delivery failures.
* **Actionable error responses.** Common send failures return specific 422 errors: invalid or landline destination numbers, opted-out recipients, destination regions not permitted by the messaging service, and suspended brand registrations. Each error includes a descriptive detail message so the caller knows what to fix.
* **Automatic delivery status tracking.** A new status callback webhook (`POST /v1/webhook/twilio/sms-status`) receives delivery lifecycle updates and projects them onto the message record. The status progresses through `pending`, `sent`, `delivered`, `failed`, or `undelivered`, with corresponding timestamps (`handed_off_at`, `delivered_at`, `errored_at`) and error codes set automatically.
* **Idempotent status callbacks.** Duplicate delivery callbacks for the same status are safely ignored. Late-arriving callbacks that would regress a terminal state (for example, a `sent` callback arriving after `delivered` or `failed`) are dropped, preventing status corruption from network reordering.
* **Sender resolution via callback.** When sending through a messaging service, the sender phone number is not known at send time. The `from_number` field on the message record is populated automatically when the first status callback reports which number the messaging service selected from the pool.

**What you need to do:**

* **To send SMS messages**, create an SMS use case with at least one phone number assigned, ensure A2P campaign verification is complete for US long-code numbers, and call `POST /v1/sms/` with the use case ID, destination number, and message body. Delivery status updates flow automatically - poll the message record or consume webhook events to track delivery.

</details>

<details>

<summary>v0.9.275 - Platform API: Per-Class Failure Delivery Routing for Background Tools (July 2026)</summary>

#### Per-Class Failure Delivery Routing for Background Tools

Skill bindings in context graphs can now route failed tool results differently depending on the type of failure. A timeout can interrupt the conversation immediately while a generic error is folded in silently on the next turn - all configured declaratively on the state-level tool binding.

**What changed:**

* **Per-failure-class delivery overrides on tool bindings.** The tool call spec on a context graph state binding now accepts an optional failure delivery map keyed by failure class (timeout, input rejected, rounds exhausted, generic). Each entry specifies whether that class of failure should interrupt the conversation or be queued for the next turn. Classes not listed in the map fall through to the existing failure delivery setting, which itself falls through to the success delivery setting. This gives you fine-grained control - for example, interrupt on timeout but queue generic errors.
* **Three-tier delivery precedence.** Delivery routing for failed tool results now follows a clear precedence chain: per-class override (if the failure class is mapped), then the binding's failure delivery setting, then the binding's success delivery setting. This is fully backward-compatible - bindings without a per-class map behave exactly as before.
* **Cancellation excluded from per-class routing.** A cleanly cancelled tool (e.g. a skill that honored a cancellation request) is not a failure class and never reads the per-class map. Cancelled results always route by the failure delivery floor, preventing a per-class override like "interrupt on generic error" from accidentally interrupting a clean cancellation.
* **Voice and text paths both honor per-class routing.** The per-class delivery override is respected on both the voice turn pipeline and the background text re-entry path, so behavior is consistent across channels.

**What you need to do:**

* **No action required for existing configurations.** Bindings without a per-class failure delivery map are unchanged - the existing failure delivery and delivery settings continue to work as before. To use per-class routing, add a failure delivery map to the tool call spec on the relevant context graph state binding. The map is optional and partial - you only need to list the classes you want to override.

</details>

<details>

<summary>v0.9.274 - Platform API: Tiered Failure Classification for Background Tools (July 2026)</summary>

#### Tiered Failure Classification for Background Tools

Background tools (non-blocking skills that run while the conversation continues) now carry structured failure classifications, giving the agent specific context about why a tool failed so it can respond more helpfully.

**What changed:**

* **Failure classification on background tool results.** When a background tool fails, the result now includes a failure class indicating the reason: timeout, input rejected (request too large), step limit exhausted (the skill hit its internal round cap), or general error. Previously, all failures were reported with a generic error message regardless of cause.
* **Failure-aware agent steering.** The agent now receives tailored guidance based on the failure class. For timeouts, it may retry or inform the caller of a delay. For input rejections, it avoids retrying the same request unchanged. For step limit failures, it considers narrowing the request. For general errors, it decides whether to retry or inform the caller. This replaces the previous one-size-fits-all failure message.
* **Task status queries surface failure reason.** When the agent checks the status of a failed background task (via the built-in task status tool), the response now includes the failure class alongside the error detail. This means the agent can explain why a task failed when the caller asks, not just that it failed.
* **World events carry failure class.** Task lifecycle events emitted for observability now include the failure classification on terminal failure events, enabling external monitoring systems to distinguish timeout failures from input rejections or other error types.
* **Accurate failure detection for returned errors.** Background tools that return an error result without raising an exception (the common case for timeouts and input rejections) are now correctly classified as failures. Previously, these were sometimes recorded as successful completions because only raised exceptions triggered the failure path.

**What you need to do:**

* **No action required.** This is a behavioral improvement to how the agent handles and reports background tool failures. There are no API contract changes. The failure classification is used internally by the agent to produce better responses and is surfaced through task status queries and observability events. Existing integrations, tool definitions, and conversation flows continue to work unchanged.

</details>

<details>

<summary>v0.9.273 - Platform API: One-Click Invitation Email (July 2026)</summary>

#### One-Click Invitation Email

Workspace invitation emails now embed a single-use magic-link that signs the invitee in and routes them directly to the invitation acceptance flow - one click instead of two separate steps (open invite link, then sign in).

**What changed:**

* **One-click sign-in link in invitation emails.** When magic-link login is enabled, invitation emails now include an "Accept & sign in" button that combines authentication and invitation acceptance into a single click. The link is single-use and carries a longer expiration window than interactive magic links, because invitation emails may sit unopened for several days.
* **Automatic fallback to standard invitation link.** If magic-link login is not enabled, or if the one-click link cannot be generated due to a transient issue, the invitation email falls back to the standard invitation link with no change in behavior. The invitee can always use the plain invitation link as a backup, even when the one-click link is present.
* **Plain invitation link always included.** The invitation email always displays a secondary link to the standard invitation page. If the one-click link expires before the invitation does (up to 7 days), the invitee can use this fallback link to sign in manually and accept.
* **Configurable one-click link expiration.** The expiration window for one-click invitation links is configured separately from interactive magic links and defaults to 72 hours. The valid range is 1 hour to 7 days - capped at the invitation lifetime so the link never outlives the invitation itself.
* **Security model unchanged.** The one-click link authenticates the invitee but does not grant workspace access by itself. The invitee must still accept the invitation (which the console handles automatically after sign-in), and the invitation must still be pending at acceptance time. Revoking the invitation invalidates the link. The bearer token in the link is never included in the plaintext email body - it appears only in the HTML link target.

**What you need to do:**

* **No action required.** This is a UX improvement for invitation emails when magic-link login is enabled. Existing invitation flows, sign-in methods, and invitation acceptance behavior continue to work unchanged. If magic-link login is not enabled for your deployment, invitation emails are unaffected.

</details>

<details>

<summary>v0.9.272 - Platform API: Email Templates and Template Send Mode (July 2026)</summary>

#### Email Templates and Template Send Mode

The email channel now supports reusable email templates with full CRUD operations, and the send email endpoint accepts a new template-based send mode alongside the existing raw HTML mode.

**What changed:**

* **Email template management.** You can now create, list, retrieve, update, and delete email templates scoped to a use case. Each template defines a subject and body layout with variable placeholders that are rendered at send time. Template names must be unique within their use case. The platform validates template syntax at creation and update time, and enforces that marketing use cases include unsubscribe link placeholders while transactional use cases do not.
* **Template send mode.** The send email endpoint now accepts a `mode` parameter with two options: `template` and `raw_html`. In template mode, you reference a saved template by ID and supply variable values as key-value pairs. The platform renders the subject and body from the template, automatically injecting unsubscribe links for marketing use cases. In raw HTML mode, the endpoint works as before - you provide the subject and body directly.
* **Template variable validation at send time.** When sending in template mode, the platform verifies that all variables declared by the template are provided in the request. Reserved unsubscribe variables cannot be passed by the caller - they are injected automatically based on the use case configuration.
* **Use case deletion gated by templates.** Deleting an email use case is now blocked while it still has associated email templates. Remove all templates before deleting the use case.

**What you need to do:**

* **Update send email calls to include `mode`.** The send email endpoint now requires a `mode` field. Set `mode` to `raw_html` and continue passing `subject` and `body_html` as before for existing behavior. To use templates, set `mode` to `template` and pass `template_id` and `template_parameters` instead.
* **Update use case deletion flows.** If you programmatically delete email use cases, ensure all associated email templates are deleted first or handle the new 409 error.

</details>

<details>

<summary>v0.9.271 - Platform API: Text Heartbeat Firer and Conversation ID Format Fix (July 2026)</summary>

#### Text Heartbeat Firer and Conversation ID Format Fix

The platform now automatically sends proactive progress updates for backgrounded text tasks, and the outbound text and heartbeat endpoints accept the correct conversation ID format for SMS conversations.

**What changed:**

* **Automatic text heartbeat firing.** When a text conversation backgrounds a long-running task, the platform now periodically checks for pending tasks and fires heartbeat requests so the agent can proactively update the customer with progress. Each pending task receives at most one heartbeat per interval, with a per-task cap that prevents a stuck task from sending unlimited messages. Once a task completes, it naturally stops receiving heartbeats. This feature ships disabled by default and can be enabled per deployment.
* **Conversation ID format corrected for SMS.** The `conversation_id` field on the outbound text and heartbeat text endpoints now accepts a string (up to 256 characters) instead of requiring a UUID. SMS conversations use a composite routing key as their conversation identifier, not a UUID. Previously, passing an SMS conversation ID to the heartbeat or outbound resume endpoints would fail because the platform tried to parse it as a UUID. Web channel conversations continue to use UUIDs as before.
* **SMS conversation resume fixed.** Resuming an SMS conversation via outbound text or heartbeat no longer fails. The platform now correctly resolves SMS conversations by their routing key rather than attempting UUID-based lookup, matching the same resolution path used for inbound SMS messages.
* **Deduplicated heartbeat firing.** Only one heartbeat is sent for a given task per interval, preventing duplicate progress messages from reaching the customer.

**What you need to do:**

* **No action required.** The conversation ID format change is backward-compatible - UUIDs are valid strings, so existing integrations that pass UUIDs for web conversations continue to work unchanged. The text heartbeat firer ships disabled by default. If you are using the heartbeat text endpoint with SMS conversations and previously encountered errors, those calls will now succeed.

</details>

<details>

<summary>v0.9.270 - Platform API: Invited Operators Can Sign In via Email (July 2026)</summary>

#### Invited Operators Can Sign In via Email

Operators invited to a workspace can now complete sign-in using email-based credentials (magic link or email OTP) even when they have no existing account. Previously, invitation-based login only worked for Google OAuth users - invited operators without Google accounts were refused at sign-in. Now, any invited operator who proves control of the invited email address can bind to the pending invitation and receive a bootstrap token.

**What changed:**

* **Email-credential invitation binding.** When an operator signs in with a magic link or email OTP and no account exists for that email, the platform now checks for pending workspace invitations addressed to that email. If a pending invitation exists, the platform creates an email-native identity and issues a scope-less bootstrap token - the same flow that already existed for Google OAuth users.
* **Consistent identity resolution across sign-in methods.** The identity created during invitation-based login uses a deterministic identifier derived from the email address, matching the scheme used by other sign-in paths. This means an operator who first signs in via email and later connects a Google account resolves to the same identity with no split-brain.
* **No domain auto-provisioning.** Invitation-based login is strictly invitation-bound. An unknown operator with no pending invitation is still refused, regardless of which sign-in method they use. Proving control of an email address alone is not sufficient to create an account - a workspace administrator must have sent an invitation.
* **Bootstrap token behavior unchanged.** The bootstrap token issued after invitation-based login carries no workspace scopes. The operator must accept the invitation through the Developer Console before receiving a workspace-scoped token. This behavior is identical across all sign-in methods.

**What you need to do:**

* **No action required.** This is a fix for invited operators who do not use Google sign-in. Existing Google OAuth flows, magic link sign-in, email OTP sign-in, and all other authentication paths continue to work unchanged. Operators who were previously unable to accept invitations via email credentials can now do so.

</details>

<details>

<summary>v0.9.269 - Platform API: Background Task Cancellation for Text Sessions (July 2026)</summary>

#### Background Task Cancellation for Text Sessions

Background tasks dispatched during text conversations can now be cancelled cooperatively. The agent can request cancellation of a previously dispatched background task, and the running task stops gracefully at its next checkpoint.

**What changed:**

* **New cancel tool available during background work.** When background tasks are in flight, the agent now has access to a cancellation tool alongside the existing status-check tool. The agent can request cancellation of any running background task by referencing its task ID.
* **Cooperative cancellation model.** Cancellation is a request, not a hard kill. A running background task checks for cancellation requests at natural checkpoints (between reasoning rounds) and exits gracefully when one is found. If the task completes before checking, the cancellation request is ignored.
* **Cross-turn cancellation.** Cancellation requests are scoped to the conversation, not the current request. A task dispatched in one turn can be cancelled in a later turn - for example, if the caller changes their mind after the original request returns.
* **Distinct cancelled status.** Cancelled tasks report a "cancelled" status rather than "failed" or "completed", so the agent and downstream consumers can distinguish between tasks that errored, tasks that finished normally, and tasks that were stopped on request.
* **Self-cleaning cancellation flags.** Cancellation request flags expire automatically, so stale requests do not accumulate.
* **Independent lifecycle preserved.** Background tasks continue to run independently of the session lifecycle. Session or request end does not cancel running tasks; explicit cancellation and runtime shutdown still bound them.

**What you need to do:**

* **No action required.** This is an additive capability. Existing background task behavior, text sessions, and voice calls continue to work unchanged. The cancellation tool appears automatically when background tasks are active.

</details>

<details>

<summary>v0.9.268 - Platform API: Voice Heartbeat - Proactive Progress During Background Tasks (July 2026)</summary>

#### Voice Heartbeat - Proactive Progress During Background Tasks

Voice calls no longer go silent while a background task is running. The agent now proactively speaks context-aware progress updates ("still pulling your records") instead of letting the silence-hangup countdown march toward a disconnect mid-task.

**What changed:**

* **Proactive heartbeat during background work.** When the caller is silent and a background task is still running, the agent now speaks a heartbeat progress update instead of staying silent. Each heartbeat resets the silence-hangup countdown, because a caller waiting on a genuine task is not absent.
* **Bounded heartbeat cap.** Heartbeats are capped at a fixed maximum per silent stretch. Once the cap is reached, the agent stops heartbeating and falls back to normal silence handling. This prevents a stuck or runaway task from keeping the call alive indefinitely.
* **Automatic budget reset.** The heartbeat budget resets whenever the caller speaks or the background task completes, so subsequent long-running tasks get a fresh set of proactive updates.
* **Context-aware updates.** Heartbeat utterances are generated through the reasoning engine with active tool context, so progress updates are specific to the task in flight rather than generic filler.
* **Text session safety.** Heartbeat processing in text sessions enforces a strict safety rule - a heartbeat can report progress or stay silent, but it can never terminate the conversation. Any termination signal produced during heartbeat evaluation is automatically discarded.

**What you need to do:**

* **No action required.** This is a behavioral improvement to voice call handling. Existing voice configurations, text sessions, and integrations continue to work unchanged. Callers waiting on background tasks will now hear progress updates instead of silence.

</details>

<details>

<summary>v0.9.267 - Platform API: Heartbeat Status Reports for Text Conversations (July 2026)</summary>

#### Heartbeat Status Reports for Text Conversations

The platform can now proactively push progress updates on in-flight text conversations. When a background task is running on behalf of a customer, the agent can check in with a status report without waiting for the customer to ask.

**What changed:**

* **New heartbeat text capability.** Trusted callers (triggers, connectors, platform-api) can now request a progress check-in on an existing text conversation. The agent resumes the conversation, evaluates whether any background work is worth reporting on, and - if so - sends an SMS update to the customer.
* **Three possible outcomes.** A heartbeat produces one of three results:
  * **Sent** - the agent had something useful to report and the SMS was delivered.
  * **No report** - the agent determined there was nothing new to say. No SMS is sent - silence is a valid outcome for a heartbeat.
  * **Contended** - a live user turn is already active on the conversation, so the heartbeat yields without preempting. The caller can retry on the next interval.
* **Conversation resume required.** Unlike outbound text initiation, a heartbeat requires an existing conversation ID. The conversation is resumed with full history so the agent's status report is grounded in prior context.
* **Stateless and idempotent.** The heartbeat endpoint is a single check-in. Periodic firing (every N seconds while a task is pending, with a max-fires cap) is the responsibility of the trigger or cron engine that dispatches to this endpoint, keeping the endpoint itself stateless.
* **Same trust model and delivery path.** Authentication, authorization, provider support, and SMS delivery follow the same model as agent-initiated outbound text. Ships dark until the same infrastructure configuration is provisioned.

**What you need to do:**

* **No action required for existing integrations.** This is a new capability. Existing outbound text, inbound SMS, and text sessions continue to work unchanged.
* **To use heartbeat status reports:** contact your Amigo representative to enable trigger-based heartbeat scheduling for your deployment. The underlying outbound text infrastructure must already be configured.

</details>

<details>

<summary>v0.9.266 - Platform API: Agent-Initiated Outbound SMS (July 2026)</summary>

#### Agent-Initiated Outbound SMS

The platform now supports unprompted agent-initiated outbound SMS. The agent can proactively send a text message to a customer without waiting for an inbound message, using the full reasoning engine to generate the opening message.

**What changed:**

* **Managed outbound text path.** Supported platform workflows can initiate an agent-generated SMS to a specified customer. The path runs the agent's greeting logic with its eligible context and tools, then delivers the generated message over SMS.
* **Conversation continuity.** The outbound text creates a conversation that can be resumed when the customer replies. An existing conversation ID can be provided to continue a prior thread.
* **Instruction steering.** Callers can pass an optional instruction to steer what the agent opens with, giving upstream systems control over the outreach context (e.g., "remind the patient about their upcoming appointment on Thursday").
* **Provider support.** Outbound text supports Twilio, Infobip, and Sendblue providers, resolved from the service number's configuration.
* **Ships dark by default.** The feature requires explicit infrastructure configuration before it becomes available. Until configured, the endpoint returns a service-unavailable response.
* **Validated configuration.** The platform validates that outbound text delivery settings are either fully configured or fully absent at startup, preventing half-configured deployments from silently failing.

**What you need to do:**

* **No action required for existing integrations.** This is a new capability. Existing inbound SMS, outbound calls, and text sessions continue to work unchanged.
* **To use outbound text:** contact your Amigo representative to enable the feature for your deployment. Once enabled, outbound text can be triggered through platform-api, connectors, or trigger workflows.

</details>

<details>

<summary>v0.9.265 - Platform API: Tiered Failure Delivery for Tool Results (July 2026)</summary>

#### Tiered Failure Delivery for Tool Results

Tool bindings in context graphs now support independent delivery routing for failed tool results, separate from successful results. This lets you configure different delivery behavior depending on whether a tool call succeeded or failed.

**What changed:**

* **New `failure_delivery` field on tool call bindings.** Each tool binding in a context graph state can now specify a `failure_delivery` value (`interrupt` or `queue`) independently of the existing `delivery` field. When a tool call fails, the platform uses `failure_delivery` to determine how the error result is routed back to the agent. When `failure_delivery` is not set, it inherits the value of `delivery`, so all existing bindings behave identically without changes.
* **Independent success and failure routing.** You can now express patterns like "fold the result in silently when the tool succeeds, but surface the error live when it fails" by setting `delivery: queue` and `failure_delivery: interrupt`, or the reverse. This gives you fine-grained control over how the agent handles tool outcomes in voice conversations.
* **Voice path honored immediately.** On the live voice path, `failure_delivery` controls whether a failed tool result interrupts the agent or is queued for the next natural pause, independent of how successful results are delivered.
* **Text path consistency.** On the text session path, both delivery axes currently collapse to next-turn injection. The `failure_delivery` value is honored for intra-batch ordering of pending results, maintaining consistency with the voice path semantics.

**What you need to do:**

* **No action required for existing configurations.** All existing tool bindings continue to work unchanged. The `failure_delivery` field defaults to inheriting the `delivery` value when not set.
* **To adopt tiered failure delivery:** add `failure_delivery` to any tool binding in your context graph where you want failed results routed differently from successful ones.

</details>

<details>

<summary>v0.9.264 - Platform API: Passwordless Magic-Link Login (July 2026)</summary>

#### Passwordless Magic-Link Login

The identity service now supports passwordless magic-link login as an alternative to the existing email one-time passcode (OTP) flow. Instead of typing a 6-digit code, users click a single-use link delivered to their email to authenticate.

**What changed:**

* **New initiation endpoint.** `POST /auth/email/magic-link/start` accepts an email address and intent (`operator` or `provider`). For provider intent, a workspace ID is required. The endpoint always returns 200 regardless of whether the email is recognized, preventing address enumeration. Rate limits apply per IP and per email address.
* **New grant type `magic_link`.** The `/token` endpoint now accepts `grant_type=magic_link` with a `magic_link_token` form parameter. The token is self-authenticating - it carries the verified email, intent, and optional workspace from when the link was minted - so no additional email or intent fields are needed on the token request.
* **Single-use, high-entropy tokens.** Each magic link contains a 256-bit token that can only be redeemed once. After a single use, the token is atomically consumed and cannot be replayed. Tokens expire after a configurable TTL (default 15 minutes, maximum 48 hours).
* **Same principal resolution as email OTP.** A magic-link login produces the same identity principal as an email OTP login for the same address. Operator intent resolves against existing federation identities and workspace memberships. Provider intent resolves against provider access grants behind the same feature flag used by other provider login flows.
* **Link delivered in URL fragment.** The magic link places the token in the URL fragment (`#token=...`) rather than the query string, so it does not appear in server access logs, load balancer logs, or cross-origin `Referer` headers.
* **OpenID discovery updated.** The `/.well-known/openid-configuration` response now advertises `email_otp` and `magic_link` in the `grant_types_supported` list.
* **IP allowlist enforcement tightened.** When a workspace has an IP allowlist configured but the client IP cannot be determined, the request is now denied rather than allowed. This closes a gap where stripping forwarded headers could bypass a configured allowlist.
* **JWT key rotation misconfiguration is now a hard failure.** Configuring a previous signing key without a rotation timestamp now prevents the service from starting, rather than logging a warning. This avoids an unbounded grace window where a retired key would verify tokens indefinitely.

**What you need to do:**

* **No action required for existing integrations.** The email OTP flow continues to work unchanged. Magic-link login is an additional option available to clients that prefer a one-click experience.
* **To adopt magic links:** call `POST /auth/email/magic-link/start` with the user's email, then exchange the token from the email link at `POST /token` with `grant_type=magic_link` and `magic_link_token=<token>`.

</details>

<details>

<summary>v0.9.263 - Platform API: Background Tool Re-Entry for Text Sessions (July 2026)</summary>

#### Background Tool Re-Entry for Text Sessions

Background tools dispatched during text sessions now survive beyond the originating request and deliver their results on the next conversation turn, so the agent can acknowledge completed work.

**What changed:**

* **App-scoped background task lifetime.** Background tools dispatched during a text session are now anchored to the application lifecycle rather than the individual request. Previously, a background tool that outlived its originating request could be collected before completing. Tasks are now retained until they finish and are cleaned up at application shutdown.
* **Reactive result delivery on next turn.** When a background tool completes between turns, its result is buffered durably. At the start of the next inbound turn, the platform drains any pending background tool results and folds them into the conversation before processing the new message. This means the agent sees and acknowledges background tool outcomes on the very next interaction.
* **Bounded catch-up per request.** To keep per-request latency predictable, the platform processes a bounded number of pending background tool results at the start of each turn. This prevents a burst of completed background tools from delaying the response.
* **Consistent with voice behavior.** Text sessions now follow the same background tool completion pattern used in voice calls - tools run to completion regardless of request boundaries, and results are delivered as soon as the agent has the floor.

**What you need to do:**

* **No action required.** This is an automatic improvement to background tool handling in text sessions. Background tools configured in your context graphs continue to work without changes. Results that previously could be lost between requests are now delivered reliably on the next turn.

</details>

<details>

<summary>v0.9.262 - Platform API: Cross-Channel Background Task Status Checking (July 2026)</summary>

#### Cross-Channel Background Task Status Checking

Background tool dispatch now includes a built-in status-checking tool that lets the agent report task progress to the caller, with consistent behavior across voice and text channels.

**What changed:**

* **Background task status registry.** When a background tool is dispatched, the platform registers it in a status registry that tracks whether each task is running, completed, or failed. The agent can query this registry to check progress and relay it during the conversation.
* **Automatic status tool availability.** While at least one background task is in flight, the platform offers a status query tool to the agent. When all tasks have finished, the tool is removed so the agent does not see it on idle turns. This keeps the agent's tool set clean and contextually relevant.
* **Cross-channel parity.** The status-checking capability works identically in voice calls and text sessions. Both channels use the same progress-tracking and query mechanism, so agents report background task status consistently regardless of the conversation channel.
* **Terminal result reporting.** When a background task completes or fails, its outcome is recorded in the registry. Status queries for finished tasks return the result along with whether the task succeeded or failed. Terminal results are capped in length to prevent large payloads from being re-injected into the conversation.

**What you need to do:**

* **No action required.** This is an automatic enhancement to background tool dispatch. The status tool appears and disappears based on whether background tasks are active. Existing background tool configurations continue to work without changes.

</details>

<details>

<summary>v0.9.261 - Platform API: Background Tool Depth Cap and Voice Floor Budget (July 2026)</summary>

#### Background Tool Depth Cap and Voice Floor Budget

Background tool dispatch now caps re-entry depth to prevent runaway continuation chains, and blocking tools in voice calls are converted to background completions if they exceed the floor dead-air budget.

**What changed:**

* **Background tool re-entry depth cap.** When a background tool completes and triggers a continuation that dispatches another background tool, the platform now tracks the depth of this chain. At the cap (depth 3), further background tools run inline instead of deferring, so the chain converges rather than cascading indefinitely. Completions queued before this change re-enter at depth 0 and self-heal within one additional level.
* **Voice floor dead-air budget for blocking tools.** Blocking tools in voice calls are now subject to a floor dead-air budget. If a blocking tool does not complete within the budget, it is automatically converted to a background completion so the agent can resume speaking with filler and a continuation turn. This prevents prolonged silence on the line while waiting for slow tool execution. The tool itself is not cancelled - it continues running and its result arrives as a background completion.
* **Background tool duration observability.** Background tool execution duration is now tracked and surfaced for observability. Tools that run longer than expected produce a warning for monitoring, but no defensive timeout is imposed - background tools are not on a latency-critical path.

**What you need to do:**

* **No action required.** These are automatic safety improvements. Background tool chains that previously could cascade indefinitely now converge at depth 3. Blocking tools that exceed the floor budget are handled gracefully without caller-visible errors. Existing configurations continue to work without changes.

</details>

<details>

<summary>v0.9.260 - Platform API: Per-State Tool Dispatch Mode (July 2026)</summary>

#### Per-State Tool Dispatch Mode

Tool calls can now be configured as blocking or background on a per-state basis within context graphs. This lets you control whether the agent waits for a tool result inline before continuing, or dispatches it in the background and keeps the conversation flowing.

**What changed:**

* **New `execution` field on tool call specs.** Each tool binding within a context graph state now supports an `execution` field with two possible values: `blocking` and `background`. `blocking` causes the agent to await the tool result inline within the current turn - the result lands before any further agent speech. `background` (the default) dispatches the tool as a background task so the agent keeps the floor and acknowledges the request while the tool runs. The result arrives asynchronously based on the tool's delivery mode.
* **Per-state, not per-tool.** The execution mode is a use-time concern bound to the state-tool binding, not to the tool definition itself. The same tool can be configured as `blocking` in one state (where the answer must precede further speech) and `background` in another state (where holding phrases are appropriate). This follows the same pattern as the existing `delivery` field.
* **Text and voice channels both supported.** Both text sessions and voice calls honor the per-state execution mode. Voice calls await blocking tools inline within the current turn. Text sessions dispatch background tools off-turn and receive results through the signal queue as completion events, which trigger a fresh engage round so the agent responds with the result.
* **Default preserves existing behavior.** The default value is `background`, which matches the historical always-async dispatch behavior. Existing configurations continue to work without changes.
* **Orthogonal to delivery mode.** The execution mode is independent of the delivery mode (`interrupt` vs `queue`). A blocking tool still respects its configured delivery mode once the awaited result is ready.

**What you need to do:**

* **No action required for existing configurations.** The default `background` execution mode preserves current behavior. To take advantage of blocking dispatch, update tool bindings in your context graph states to set `execution` to `blocking` where you want the agent to wait for the result before continuing.

</details>

<details>

<summary>v0.9.259 - Platform API: Distinct Failed Status for Tool Calls (July 2026)</summary>

#### Distinct Failed Status for Tool Calls

Tool calls that error or are rejected now report a `failed` status instead of being marked as `completed`. This is the first phase of non-blocking tool support.

**What changed:**

* **New `failed` tool call status.** Tool calls that encounter an error during execution or are rejected now carry a `failed` status in the tool status tracker. Previously, all finished tool calls were marked `completed` regardless of outcome. The possible tool call statuses are now `queued`, `running`, `completed`, and `failed`.
* **Status query includes success indicator.** When you query tool call status (individually or in bulk), terminal tool calls (`completed` or `failed`) now include a `succeeded` field indicating whether the tool call finished successfully. In-progress tool calls (`queued`, `running`) do not include this field.
* **Failure-aware agent guidance.** When a tool call fails, the agent receives context-appropriate guidance that steers it toward retry, alternative approaches, or informing the caller - rather than treating the result as a successful completion. This guidance respects the same delivery modes (interrupt vs. queued) used for successful completions.
* **Eviction covers failed entries.** The tool status memory management now evicts both `completed` and `failed` entries when the status tracker reaches its capacity limit, keeping memory usage bounded.

**What you need to do:**

* **No action required for most users.** This is an internal agent behavior improvement. If you consume tool call status data through the status query tool, note that the `status` field can now return `failed` in addition to the existing values, and terminal entries include the new `succeeded` boolean field.

</details>

<details>

<summary>v0.9.258 - Platform API: Text Interact Timeout Raised to 180s (July 2026)</summary>

#### Text Interact Timeout Raised to 180s

The text interact endpoint now allows requests up to 180 seconds before timing out, up from the previous 60-second limit. The server-side timeout within the agent engine has been removed entirely - the caller-side timeout is now the only enforced cap.

**What changed:**

* **Caller timeout raised to 180s.** The platform API proxy timeout for text interact requests has been increased from 60 seconds to 180 seconds. This accommodates slow upstream integrations such as external clinical data APIs or partner-hosted endpoints that may take longer than 60 seconds to respond.
* **Server-side timeout removed.** The agent engine no longer enforces its own timeout on text interact processing. Previously, the engine applied a 60-second cap independently of the caller. Now, the caller-side timeout is the single contract governing how long a text interact request can run.
* **Streaming text interact also affected.** The streaming variant of text interact follows the same change - the server-side stream timeout has been removed, and the caller-side timeout governs the maximum duration.

**What you need to do:**

* **No action required.** Existing text interact calls continue to work. Requests that previously timed out at 60 seconds due to slow upstream dependencies may now complete successfully. If you have client-side timeouts set below 180 seconds, you may want to raise them to take advantage of the extended window.

</details>

<details>

<summary>v0.9.257 - Platform API: Simulation Suite Run Results (July 2026)</summary>

#### Simulation Suite Run Results

Simulation suites now support durable suite runs with dedicated endpoints for listing past runs and retrieving aggregate results.

**What changed:**

* **New endpoint: list suite runs.** `GET /simulations/suites/{suite_id}/runs` returns a paginated list of past suite runs for a given suite, ordered by most recent first. Each entry includes a suite run ID, status, status breakdown by individual run, case IDs, session and turn totals, tags, and timestamps. Use the `limit` query parameter (1-50, default 20) to control page size.
* **New endpoint: get suite run results.** `GET /simulations/suite-runs/{suite_run_id}` returns aggregate results for a specific suite run. The response includes the same scoring, capability breakdowns, and metric availability fields as benchmark results, plus a `summary` object with the suite run metadata and a `suite_id` field linking back to the parent suite. Returns 404 if the suite run does not exist or if any constituent runs cannot be resolved.
* **Suite runs are tagged automatically.** Each suite run triggered through the suite run or benchmark endpoint is now tagged with a durable suite run identifier, so all constituent case runs can be grouped and queried as a unit.

**What you need to do:**

* **No action required.** These are additive endpoints. Existing suite and benchmark workflows are unchanged. Use the new endpoints to build suite run history views, track regression trends over time, or integrate suite run results into CI pipelines.

</details>

<details>

<summary>v0.9.256 - Platform API: Data Source Status and Sync History Now Report Live Event Counts (July 2026)</summary>

#### Data Source Status and Sync History Now Report Live Event Counts

The data source status and sync history endpoints now return live event counts derived from the platform's analytical event store, replacing the previous stub values that always returned zeros.

**What changed:**

* **Status endpoint returns live counts.** The `event_count` and `synced_count` fields on the data source status response now reflect the actual number of events recorded for that data source. Previously, these fields always returned `0` because the underlying query was stubbed out during an earlier migration. `failed_count` remains `0` under the current event model and will be populated in a future update when failure-typed events are introduced.
* **Sync history returns daily event timeline.** The sync history endpoint now returns a `timeline` array with daily event volume buckets. Each bucket includes `date`, `event_count`, `synced_count`, and `failed_count`. Days with no events are omitted from the timeline. Previously, the timeline was always empty.
* **Graceful fallback on unavailability.** If the analytical event store is temporarily unavailable, both endpoints fall back to zero-valued counts and empty timelines rather than returning errors. The data source metadata and other fields continue to be served normally.

**What you need to do:**

* **No action required.** These are the same response fields with the same types - they now contain accurate data instead of zeros. If you have dashboards or integrations that read `event_count`, `synced_count`, or `timeline` from data source status and sync history responses, they will start showing real values automatically.

</details>

<details>

<summary>v0.9.255 - Platform API: Connector Entities Support Sort Order Parameter (July 2026)</summary>

#### Connector Entities Support Sort Order Parameter

The connector entities list endpoint now accepts a `sort_by` query parameter for controlling the order of returned entities.

**What changed:**

* **New `sort_by` query parameter.** The connector entities list endpoint accepts an optional `sort_by` parameter with the following values:
  * `+display_name` (default) - alphabetical by display name, A-Z
  * `-display_name` - reverse alphabetical by display name, Z-A
  * `+last_event_at` - oldest activity first
  * `-last_event_at` - most recent activity first
* **Stable pagination.** All sort orders include a secondary tiebreaker to ensure consistent ordering across paginated requests. Entities with identical sort values or null values appear in a deterministic order.
* **Default behavior unchanged.** When `sort_by` is omitted, entities are returned in alphabetical order by display name, matching the previous behavior.

**What you need to do:**

* **No action required.** This is an additive parameter. Existing requests without `sort_by` continue to work identically. To sort by most recent activity, add `?sort_by=-last_event_at` to your connector entities list request.

</details>

<details>

<summary>v0.9.254 - Platform API: Conversation Detail Turns Include Context Graph Action and State Transition Detail (July 2026)</summary>

#### Conversation Detail Turns Include Context Graph Action and State Transition Detail

The conversation detail endpoint now returns richer turn-level data that surfaces context-graph actions and state transitions for each turn.

**What changed:**

* **`selected_action_description` added to turns.** Each turn in the conversation detail response now includes a `selected_action_description` field. This contains the human-readable description of the context-graph action the agent selected for that turn. The field is `null` when no action was selected or no selection was recorded.
* **`state_transition` added to turns.** Each turn now includes a `state_transition` object with `from` and `to` fields, indicating the context-graph state transition that occurred during the turn. The field is `null` when no transition took place. If multiple transitions occurred within a single turn, the API reports the first source state and the final destination state.
* **`available_actions` added to turns.** Each turn now includes an `available_actions` array listing the action choices available in the turn's resolved context-graph state. Each element contains a `description` field. The array is empty when actions cannot be resolved or the resolved state has no actions. When a turn does not carry inline action data, the API resolves available actions from the service's context-graph configuration.
* **Applies to both text and voice conversations.** These new fields appear on turns for all conversation types returned by the conversation detail endpoint.

**What you need to do:**

* **No action required.** These are additive fields with safe defaults (`null` for optional fields, `[]` for the actions array). Existing integrations that do not read these fields are unaffected. If you display conversation detail in a UI, you can now show which action the agent chose, what state transition occurred, and what actions were available at each turn.

</details>

<details>

<summary>v0.9.253 - Platform API: Dead Fields Removed from Data Source Responses (July 2026)</summary>

#### Dead Fields Removed from Data Source Responses

Three unused fields have been removed from data source API responses.

**What changed:**

* **`last_health_check` removed.** This field was never populated by any system process and always returned `null`. It has been removed from data source list, detail, and status responses.
* **`discovered_by` removed.** This field was set at creation time by some internal paths but was never exposed in a meaningful way to API consumers. It has been removed from data source responses.
* **`last_sync_event_count` removed.** This field was always `0` and was never updated after data source creation. It has been removed from data source responses.

**What you need to do:**

* **Update any code that reads these fields.** If your integration reads `last_health_check`, `discovered_by`, or `last_sync_event_count` from data source responses, remove those references. The fields were never populated with useful values, so removing them should not affect any working logic. If you have strict response parsing that fails on missing fields, update your schema to make these fields optional or remove them.

</details>

<details>

<summary>v0.9.252 - Platform API: Data Source Health Status Derived at Read Time (July 2026)</summary>

#### Data Source Health Status Derived at Read Time

The `health_status` field on data source responses is now derived from live signals at read time instead of returning a static value that was set at creation and never updated.

**What changed:**

* **Live health derivation.** The `health_status` field on data source list, detail, update, and status endpoints now reflects real-time connector health. The platform evaluates whether the connector process is actively running and whether the most recent sync succeeded or failed, then returns the appropriate status (`healthy`, `degraded`, or `unknown`). Previously, `health_status` was seeded to `unknown` when the data source was created and never changed, making it unreliable.
* **Polling and continuous sources** are marked `healthy` when the connector is actively running and the last sync succeeded, `degraded` when the connector has stopped reporting or the last sync failed, and `unknown` when the connector is running but has not completed its first sync.
* **Manual and webhook sources** are marked `healthy` once any successful sync has been recorded, and `unknown` otherwise. These sources have no expected polling cadence, so a missing heartbeat does not indicate degradation.
* **Outbound-only sources** (no inbound polling configured, explicit outbound binding) are marked `healthy` once a successful write-back has been recorded, and `unknown` otherwise.

**What you need to do:**

* **No action required.** The response field name and type are unchanged. If you were ignoring `health_status` because it was always `unknown`, you can now use it as a reliable signal for connector health. If you display health status in a dashboard or monitoring system, you will start seeing accurate values without any integration changes.

</details>

<details>

<summary>v0.9.251 - Platform API: Unified Source Types and Outbound-Only Staleness Fix (July 2026)</summary>

#### Unified Source Types and Outbound-Only Staleness Fix

The data sources endpoint now supports two additional source types and no longer incorrectly flags outbound-only data sources as stale.

**What changed:**

* **New source types supported.** The data sources list and detail endpoints now accept and return two additional source type values: `lakebase_schema` and `crm`. Previously, data sources with these types could cause errors when loading the data sources list because the response model did not include them.
* **Outbound-only sources are no longer flagged as stale.** Data sources that are configured exclusively for outbound writes (no inbound polling queries) are no longer marked as stale. Previously, an outbound-only source with a `continuous` or `polling` sync strategy would appear stale in the UI because its last sync timestamp only updates on outbound events, not on a regular inbound cadence. The platform now detects outbound-only configurations and exempts them from staleness checks. Misconfigured sources with no inbound or outbound bindings are still flagged as stale so operators can see something is wrong.

**What you need to do:**

* **No action required.** These changes fix incorrect behavior. Data sources that were previously erroring or appearing falsely stale will now load and display correctly. The response schema for data source endpoints is unchanged apart from the expanded set of valid source type values.

</details>

<details>

<summary>v0.9.250 - Platform API: Connector Settings and Data Sources Commit Atomically (July 2026)</summary>

#### Connector Settings and Data Sources Commit Atomically

Updating connector settings now commits the connector configuration and the derived data sources index in a single atomic transaction. Previously, these were written independently, which meant a failure during the data sources sync could leave the index out of sync with the saved connector configuration.

**What changed:**

* **Atomic commit.** When you update connector settings, the connector configuration and the derived data sources index are now written together in one transaction. If either write fails, both are rolled back. This eliminates the possibility of silent drift between connector configs and the data sources index after a partial failure.
* **No silent swallowing of sync errors.** Previously, a failure during data source index sync was logged but did not affect the API response - the connector config would be saved while the index lagged behind. Now, a sync failure rolls back the entire update and returns an error, so callers always know whether the full write succeeded.

**What you need to do:**

* **No action required.** This is a reliability improvement with no changes to the request or response shape of the connector settings endpoint. Callers that previously succeeded will continue to succeed. Callers that previously received a success response despite a silent sync failure will now receive an error, which accurately reflects the outcome.

</details>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.amigo.ai/api-reference/change-logs/amigo-api/amigo-api-archive-v0-9-250-399.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
