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

# API History: Archive 7

Retained API history, archive 7, with original entry headings and links back to current integration guidance.

Historical entries retain their original release/source context. Use [Current Upgrade Guidance](/api-reference/change-logs/upgrade-guide.md) for current integration boundaries and [API](/api-reference/change-logs/amigo-api.md) for the recent history and archive index. An older entry does not establish current availability.

<details>

<summary>Platform API: Metering - Daily Meter Grain, Invoice Summation Fix, and Connector Enrichment Metering (July 2026)</summary>

#### Metering - Daily Meter Grain, Invoice Summation Fix, and Connector Enrichment Metering <a href="#metering-daily-meter-grain-invoice-summation-fix-and-connector-enrichment-metering" id="metering-daily-meter-grain-invoice-summation-fix-and-connector-enrichment-metering"></a>

Usage metering now records meter values at daily grain instead of monthly, invoice generation correctly sums daily meter rows across a billing period, and connector enrichment calls are now metered per workspace.

**What changed:**

* **Daily meter grain.** Meter values are now emitted at daily granularity instead of monthly. Each day produces a separate meter row per meter key and metering source. This gives more precise usage visibility and supports mid-period billing adjustments.
* **Invoice summation fix.** Invoice generation now correctly sums all daily meter rows for a billing period when computing line-item quantities. Previously, only one row per meter key was used, which could under-report usage for periods spanning multiple days. Invoices now reflect the full accumulated usage.
* **Connector enrichment metering.** When the platform uses AI models during connector data enrichment, the token usage is now metered and attributed to the workspace whose data was enriched. For batch enrichment calls that process data from multiple workspaces in a single request, usage is apportioned proportionally across the workspaces based on their share of the input data. This ensures that connector enrichment costs appear in standard usage reporting alongside other metered usage.

**What you need to do:**

* **No action required.** The daily grain change is backward-compatible - billing periods still aggregate correctly, and invoices reflect the same total usage. Connector enrichment usage will begin appearing in usage reports automatically.
* **If you consume raw meter data:** Note that meter rows are now emitted daily rather than monthly. Queries that assume one row per meter key per month should be updated to sum across the billing period.

</details>

<details>

<summary>Platform API: Memory - Custom (Layer-2) Dimensions in Extraction (July 2026)</summary>

#### Memory - Custom (Layer-2) Dimensions in Extraction <a href="#memory-custom-layer-2-dimensions-in-extraction" id="memory-custom-layer-2-dimensions-in-extraction"></a>

The memory extraction pipeline now supports per-workspace custom dimensions alongside the fixed system defaults.

**What changed:**

* **Two-tier dimension model.** Memory extraction dimensions are now organized into two tiers. Layer-1 dimensions are the fixed system defaults computed for every workspace (emotional state, engagement patterns, communication preferences, and so on). Layer-2 dimensions are workspace-specific custom keys that a workspace opts into through the enrichment registry.
* **Opt-in via enrichment registry.** A custom dimension is picked up by the extractor only when its registry entry carries the `memory_extract` tag. This is an opt-in model - most enrichment keys (such as connector-fed clinical facts) are deliberately excluded from extraction because they should not be inferred from conversation transcripts.
* **Merged into the extraction pipeline.** Custom dimensions are rendered as additional targets in the extraction prompt and added to the observation key-gate, so they flow through the same extraction and validation machinery as the system defaults. No per-workspace prompt code is needed.
* **Safety constraints on custom keys.** Custom dimension keys must be valid identifiers (bare snake\_case, no special characters) and must not shadow a system default key. System defaults always take precedence - a workspace cannot replace or override built-in dimensions like emotional state or crisis indicators. Custom dimensions are capped at the behavioral precision tier, so they do not affect safety-critical signal routing.
* **Deterministic prompt ordering.** Custom dimensions are sorted by key before rendering, which stabilizes prompt layout for tests and debugging. Extraction still uses a model, so the resulting observations can vary across runs.

**What you need to do:**

* **To use custom dimensions:** Register person-entity enrichment keys in your workspace's enrichment registry with the `memory_extract` tag and a text description. The extractor will pick them up automatically on its next run.
* **No action required for existing workspaces.** Workspaces with no custom dimensions continue to use the system defaults exactly as before. There is no change to existing extraction behavior.

</details>

<details>

<summary>Platform API: Agent Runs - Native Definition Validation (July 2026)</summary>

#### Agent Runs - Native Definition Validation <a href="#agent-runs-native-definition-validation" id="agent-runs-native-definition-validation"></a>

Native agent definitions are now validated through a strict whitelist schema that treats customer-authored definitions as untrusted input.

**What changed:**

* **Whitelist clamp validation.** Every native definition is parsed through a strict schema that accepts only the fields the platform will honor. Any unrecognized field is a validation error naming the offending field - fields are never silently ignored or rewritten. This prevents configuration injection through unexpected keys such as subprocess environment variables, custom tool URLs, or sampling parameters.
* **Tool name validation.** The `allowed_world_tools` list on each actor in a native definition must reference tools from the platform catalog only. An unknown tool name is rejected with a 422 identifying the invalid name. Tools are referenced by catalog name - there is no passthrough to arbitrary tool endpoints.
* **Write tool tagging.** When a native definition references any write tool, the definition is tagged with a write indicator. This drives the write badge in the console and the write-scope enforcement at the data surface boundary.
* **Bounded fan-out.** Native definitions enforce caps on the number of agents, tasks, turns, and iterations to limit resource consumption per run.
* **No platform identifiers in the body.** The schema rejects platform identifiers (workspace ID, entity ID, service ID) in the definition body, preventing cross-workspace reference injection.
* **Model-family validation.** When a native definition pins a specific model, the platform validates that the model belongs to the correct provider family for the chosen framework. A mismatched model is rejected at registration rather than failing at run time.
* **Internal consistency checks.** Task-to-agent references, agent handoff targets, entry agent resolution, and text placeholders are all validated for internal consistency at registration time.
* **Schema revision tracking.** Each validated definition is stamped with a schema revision so that if the accepted shape changes in the future, definitions that no longer parse are diagnosable with a clear error naming the offending path and the revision.

**What you need to do:**

* **Review native definitions for unrecognized fields.** If your native definition includes fields outside the supported set, they will now be rejected. Remove any unsupported fields before registering.
* **Verify tool names.** Ensure all `allowed_world_tools` entries reference valid platform catalog tool names. Use the read dispatcher tools (`world_read`, `list_world_read_tools`) for read access, and named clinical write tools for write access.
* **Check model pinning.** If you pin a model in your native definition, confirm it belongs to the correct provider family for your chosen framework.

</details>

<details>

<summary>Platform API: Agent Runs - Native Run Mode (July 2026)</summary>

#### Agent Runs - Native Run Mode <a href="#agent-runs-native-run-mode" id="agent-runs-native-run-mode"></a>

Agent runs now support a native run mode where a customer's own agent definition drives the run instead of a platform-authored persona and context graph.

**What changed:**

* **Native run context.** When dispatching a run in native mode, the run context carries the workspace scope, tool surface, and write-scope bindings but omits the platform persona and context graph. Identity and instructions come from the customer's own agent definition (their own framework graph or crew) rather than from a platform-authored agent version.
* **Server-side safety enforcement.** Native runs enforce safety at the data surface boundary - the tool surface and write-scope bindings are applied server-side, so customer-authored agents cannot bypass workspace data access controls regardless of how they are configured.
* **Entry actor resolution.** For native runs, the entry actor name is resolved from the native definition body at the framework edge rather than from a platform persona. Trajectory normalization uses a neutral default when no platform persona is present.

**What you need to do:**

* **No action required for existing runs.** Platform-mode runs (with a persona and context graph) behave identically. The native run mode is an additional option for customers who bring their own agent definitions.
* **Native run users: note the safety boundary.** Your agent definition controls identity and instructions, but workspace data access is still governed by the tool surface and write-scope bindings configured on the workspace.

</details>

<details>

<summary>Platform API: Agent Runs - Actor Attribution and Multi-Agent Trajectory (July 2026)</summary>

#### Agent Runs - Actor Attribution and Multi-Agent Trajectory <a href="#agent-runs-actor-attribution-and-multi-agent-trajectory" id="agent-runs-actor-attribution-and-multi-agent-trajectory"></a>

Agent run trajectories now carry per-step actor attribution and capture agent-to-agent delegation events, enabling cross-framework analytics over multi-agent runs.

**What changed:**

* **Actor attribution on every trajectory step.** Each step in the trajectory now includes an `actor_name` field identifying which actor within the run produced the step. For single-agent runs, every step attributes to the entry agent. For multi-agent definitions (runs with subagents or delegated agents), steps attribute to the specific actor that produced them. The field is always populated - it never defaults to an empty string - so grouping and filtering by actor is reliable across frameworks.
* **Delegation handoff steps.** When one agent delegates to another within a multi-agent run, the trajectory now includes a `handoff` step with `handoff_kind` set to `delegation`. The step's `actor_name` identifies the source (delegating) agent, and a new `handoff_to` field identifies the target agent. Previously, these delegation events were silently dropped from the trajectory.
* **Framework identifier on trajectory metadata.** The trajectory reference now includes a framework identifier so consumers can distinguish which supported framework executed the run without inspecting step-level details.

**What you need to do:**

* **API consumers: note new fields.** If you consume the run detail or trajectory response, two new fields are available on each trajectory step: `actor_name` (string, always present) and `handoff_to` (string, present on delegation handoff steps). Both are safe to ignore if you do not need multi-agent analytics.
* **No action required for single-agent runs.** Single-agent runs behave identically - every step attributes to the entry agent, and no delegation handoff steps appear.

</details>

<details>

<summary>Platform API: Intake Files - Hide Withdrawn Documents (July 2026)</summary>

#### Intake Files - Hide Withdrawn Documents <a href="#intake-files-hide-withdrawn-documents" id="intake-files-hide-withdrawn-documents"></a>

The intake files list endpoint now hides files whose parent document has been withdrawn (source-deleted) by default, keeping the Files list clean when upstream sources remove content.

**What changed:**

* **New `include_withdrawn` query parameter.** The list intake files endpoint now accepts an `include_withdrawn` boolean query parameter (defaults to `false`). When `false`, files belonging to withdrawn documents are excluded from the response. When `true`, all files are returned regardless of document status.
* **Default behavior change.** Previously, files from withdrawn documents appeared in the files list with no indication that their parent document had been removed. Now these files are hidden by default, so the files list reflects only active content.
* **Files without a parent document are unaffected.** Snapshot and CSV files that are not linked to a parent document continue to appear in the list regardless of the `include_withdrawn` setting.

**What you need to do:**

* **No action required for most users.** The default behavior now excludes withdrawn files, which matches the expected experience. If you need to see files from withdrawn documents, pass `include_withdrawn=true` to the list endpoint.

</details>

<details>

<summary>Platform API: Agent Runs - Dual-Mode Auth, Cache Usage, and Hardening (July 2026)</summary>

#### Agent Runs - Dual-Mode Auth, Cache Usage, and Hardening <a href="#agent-runs-dual-mode-auth-cache-usage-and-hardening" id="agent-runs-dual-mode-auth-cache-usage-and-hardening"></a>

Agent runs now accept operator identity tokens (the console path) in addition to workspace API keys, report granular cache token usage, and include several reliability improvements.

**What changed:**

* **Dual-mode authentication.** Agent run endpoints now accept both workspace API keys and operator identity tokens. The console's Framework Runs panel uses an identity token derived from the operator's session - this path was previously rejected. The bearer is forwarded end-to-end and re-verified by the data surface, so the caller's credential governs data access throughout the run.
* **Cache token usage.** Run results now include `cached_tokens` (cache-read input tokens) and `cache_creation_tokens` (cache-write input tokens) alongside the existing `input_tokens` and `output_tokens`. These fields reflect native provider accounting where cache reads and writes are billed at different rates. Without them, a cache-heavy run reported a misleading near-zero input count.
* **Trajectory span deduplication.** Trajectory span IDs now include a per-run component, preventing span-ID collisions when the same workspace dispatches multiple runs. This fixes duplicate event detection in downstream event buses.
* **MCP tool call timeout.** The timeout for each tool call over the world-tools data surface has been increased to accommodate data queries and cold starts that routinely exceeded the previous default.
* **Improved error messages.** Failed runs now surface actionable error text instead of opaque internal exception strings. Grouped exceptions (such as connection failures during tool calls) are flattened to their leaf messages.

**What you need to do:**

* **Console users: no action required.** The Framework Runs panel on the service detail page now works with your console session - no API key needed.
* **API consumers: update usage parsing.** If you consume the run detail response, note the two new fields in the `usage` object: `cached_tokens` and `cache_creation_tokens`. Both default to `0` and are safe to ignore if you do not need cache-granular cost tracking.

</details>

<details>

<summary>Platform API: OIDC/SAML Federation - Consumed-Invitation Recovery (July 2026)</summary>

#### OIDC/SAML Federation - Consumed-Invitation Recovery <a href="#oidc-saml-federation-consumed-invitation-recovery" id="oidc-saml-federation-consumed-invitation-recovery"></a>

OIDC and SAML federation login now recovers operators who accepted a workspace invitation through a non-federation method (such as magic link or email one-time password) before signing in with their workspace's federated identity provider. Previously, these operators were rejected with a 401 because their identity was recorded under a different authentication provider, and the federation login could not resolve them.

**What changed:**

* **Source-workspace recovery path.** When a federated login cannot resolve an operator through the standard per-provider identity lookup or a provision policy, the platform now checks whether the asserted email matches an existing active member of the federation source's own workspace. If it does, the login succeeds and the issued token is scoped to that workspace.
* **Token scoped to source workspace.** Tokens issued through the recovery path are always scoped to the federation source's own workspace - never to a client-supplied workspace ID. This prevents a workspace-scoped identity provider from minting tokens for workspaces outside its boundary.
* **No durable identity created.** The recovery path does not create a persistent federation identity mapping. Each subsequent login re-verifies active membership in the source workspace, so the operator must remain an active member for future logins to succeed.
* **Pending invitations not auto-accepted.** Unlike the inbox-proof authentication methods (magic link, email one-time password), the federation recovery path does not bind pending invitations from an identity provider assertion. Invited users still accept through the standard console flow, which verifies the invited email independently.
* **Audit logging.** Recovery-path logins are recorded with a dedicated audit event that includes the federation source, the matched authentication provider, and the asserted email.

**What you need to do:**

* **No action required.** Operators who were previously unable to log in through their workspace's OIDC or SAML provider after accepting an invitation via magic link or email one-time password can now sign in without manual intervention. No configuration changes are needed.

</details>

<details>

<summary>Platform API: Workspace-Bound Admin Routes and platform:admin Scope (July 2026)</summary>

#### Workspace-Bound Admin Routes and platform:admin Scope <a href="#workspace-bound-admin-routes-and-platform-admin-scope" id="workspace-bound-admin-routes-and-platform-admin-scope"></a>

All identity admin API routes are now workspace-bound. Admin callers can only act on resources within their own workspace unless they hold the new global `platform:admin` scope. This closes a class of cross-tenant authorization gaps where a workspace admin could target another workspace's resources.

**What changed:**

* **New `platform:admin` scope.** A new global scope (`platform:admin`) grants cross-workspace admin authority over the identity admin API. This scope is never granted through workspace role expansion - it must be explicitly provisioned to platform-team credentials. It is non-delegatable: credentials cannot pass it to downstream grants.
* **Workspace-bound admin routes.** Every admin route that reads or writes a workspace-scoped resource now verifies the caller is an admin of that specific workspace (by comparing the token's active workspace to the target resource's workspace). Previously, holding `identity:admin` (which is granted to every workspace owner/admin via role expansion) was sufficient to act on any workspace's resources.
* **List routes pinned to caller workspace.** Admin list endpoints (credentials, federation sources, SSO connections, provision policies, users, sessions, audit log) now pin non-platform-admin callers to their own workspace. A workspace admin who omits the workspace filter sees only their own workspace's data rather than all workspaces. Explicit cross-workspace filter values are rejected for non-platform-admin callers.
* **Credential scope escalation guard.** Creating or updating a credential that carries the `platform:admin` scope now requires the caller to already hold `platform:admin`. This prevents a workspace admin from minting a credential with cross-workspace authority and self-escalating.
* **MFA reset scoped to workspace.** A workspace-bound admin's MFA reset now deletes only the target entity's enrollments within the caller's workspace, not across all workspaces. A `platform:admin` caller can reset across all workspaces.
* **Federation identity creation authorized against credential workspace.** Creating a federation identity mapping now verifies the caller is an admin of the target credential's workspace, preventing cross-tenant identity binding.
* **SSO connection cross-workspace source guard.** Creating an SSO connection now verifies the referenced federation source belongs to the same workspace as the connection, preventing a workspace's login from being bound to another workspace's identity provider.
* **Provision policy workspace enforcement.** Global provision policies (those without a workspace) require `platform:admin` to create, update, or delete - a workspace-bound admin cannot create a global policy that would auto-provision access across tenants.
* **Internal grant workspace binding.** The internal email-change grant route now verifies the caller's token is scoped to the target workspace (or holds `platform:admin`), closing a cross-tenant provider takeover path.
* **Audit log cross-workspace reads restricted.** Only `platform:admin` callers can read audit log entries across workspaces. Workspace-bound admins are restricted to their own workspace's audit entries.

**What you need to do:**

* **No action required for workspace-scoped admin usage.** If your admin credentials operate within a single workspace (the common case), existing integrations continue to work unchanged - the token's workspace already matches the resources being managed.
* **Cross-workspace admin tooling.** If you have automation that manages resources across multiple workspaces using `identity:admin`, those credentials must now also carry the `platform:admin` scope. Contact your platform team to provision this scope on the relevant credentials.
* **Credential creation with `platform:admin`.** If you programmatically create credentials that carry the `platform:admin` scope, the calling credential must itself hold `platform:admin`.

</details>

<details>

<summary>Platform API: Simulation Performance - Per-Run Metric Breakdown (July 2026)</summary>

#### Simulation Performance - Per-Run Metric Breakdown <a href="#simulation-performance-per-run-metric-breakdown" id="simulation-performance-per-run-metric-breakdown"></a>

The simulation performance endpoint now returns a per-run breakdown for each metric instead of pre-computed trend series and pass rates. This moves aggregation to the client, so switching between analysis windows (latest run, last N runs, all runs) is instant with no refetch.

**What changed:**

* **Per-run metric data replaces pre-computed series.** Each metric in the performance response now carries a `per_run` array. Each entry represents one run that exercised the metric (ordered oldest to newest) and includes the run timestamp (`at`), the mean numeric value for that run (`value`, null for non-numeric metrics), and pass/measured counts (`passed`, `measured`).
* **Removed fields.** The previous `current_value`, `series`, `delta`, `pass_rate`, and `accruing` fields on each metric have been removed. Clients should derive current values, deltas, and pass rates from the `per_run` array.
* **Non-numeric metrics included.** A metric appears in the response if it produced a numeric value or a measured verdict in at least one run. Categorical and boolean metrics that carry no numeric value still report pass and measured counts per run, so they have a computable pass rate.
* **Pending-only runs excluded.** Runs where a metric only sat in a pending state (no value and no measured verdict) do not contribute a point to the `per_run` array.

**What you need to do:**

* **Update client-side aggregation.** If you consume the performance endpoint, replace reads of `current_value`, `series`, `delta`, `pass_rate`, and `accruing` with logic that derives those values from the `per_run` array. For example, `current_value` is the `value` of the last entry, `delta` is the difference between the last two entries' values, and `pass_rate` is `sum(passed) / sum(measured)` over your chosen window.
* **No endpoint or permission changes.** The endpoint path, query parameters, and permission requirements are unchanged.

</details>

<details>

<summary>Platform API: Simulation Performance Overview - Aggregated Run Analytics (July 2026)</summary>

#### Simulation Performance Overview - Aggregated Run Analytics <a href="#simulation-performance-overview-aggregated-run-analytics" id="simulation-performance-overview-aggregated-run-analytics"></a>

A new endpoint aggregates recent graded simulation runs into a single performance overview, replacing the need to fetch each run individually and aggregate client-side.

**What changed:**

* **New performance endpoint.** `GET /v1/{workspace_id}/simulations/performance` returns an aggregated view of recent case and suite simulation runs in a single request. The response includes an overall pass rate, total evals measured, a count of cases and suites that need attention, and the number of runs analyzed.
* **Per-metric run breakdown.** Each metric tracked across runs is returned with a `per_run` array containing one entry per run that exercised the metric (oldest to newest), including the run timestamp, mean value, and pass/measured counts. Clients derive trend series, current values, deltas, and pass rates from this data.
* **Per-case rollups.** Case-kind runs are rolled up by case, showing pass rate, passed and measured counts, the most recent run ID and timestamp, and per-eval assertion breakdowns. Each assertion includes per-conversation verdicts so you can drill into rationale and cited turns without an additional request.
* **Per-suite rollups.** Suite-kind runs are rolled up by suite, with each suite containing its own per-case breakdowns in the same shape as the top-level case rollups.
* **Needs attention flagging.** Cases and suites with a pass rate below 100% are counted in the `needs_attention_count` field, making it easy to identify which areas require investigation.
* **Configurable analysis window.** The `limit` query parameter controls how many recent runs are included in the analysis (default 15, capped at 50). An optional `service_id` parameter filters to runs for a specific service.

**What you need to do:**

* **To use the performance overview**, send a GET request to `/v1/{workspace_id}/simulations/performance`. The response contains the full aggregated view. No additional per-run fetches are needed.
* **Permissions.** The endpoint requires the Service view permission, consistent with other simulation read operations.

</details>

<details>

<summary>Platform API: Agent Runs - Framework Agent Execution (July 2026)</summary>

#### Agent Runs - Framework Agent Execution <a href="#agent-runs-framework-agent-execution" id="agent-runs-framework-agent-execution"></a>

A new pair of endpoints lets you dispatch and poll partner-framework agent runs that execute unmodified against your workspace's world-tools data surface. Two frameworks are supported today: the Claude Agent SDK and the OpenAI Agents SDK. Each run uses your service's authored agent configuration (persona, context graph, version set) and accesses workspace data exclusively through the platform's world-tools surface under the caller's own credential.

**What changed:**

* **New dispatch endpoint.** `POST /v1/{workspace_id}/agent-runs` accepts a service ID, framework selection, user message, and optional timeout, and returns a run ID with status `running` (HTTP 202). The framework executes asynchronously on the server.
* **New poll endpoint.** `GET /v1/{workspace_id}/agent-runs/{run_id}` returns the run's current status (`running`, `succeeded`, `failed`, or `timed_out`), final agent text, token usage, and a normalized trajectory of the framework's native output.
* **Two supported frameworks.** `claude-agent-sdk` runs the Claude Agent SDK autonomously. `openai-agents` runs the OpenAI Agents SDK autonomously. Both frameworks run in their own design - the platform provides configuration, data access, and trajectory normalization at the boundaries.
* **Timeout budget.** Each run accepts a `timeout_s` parameter (1-300 seconds, default 120) that caps the server-side wall-clock time for the entire run. Runs that exceed the budget transition to `timed_out`.
* **Normalized trajectory.** The trajectory in the poll response is a sequence of steps (transcript, tool call, decision, completion, usage) normalized from the framework's native output. The shape is consistent across frameworks.
* **Workspace-scoped authentication.** Both endpoints require a workspace API key. The caller's credential is forwarded to the world-tools surface, so all data access executes under the caller's own workspace permissions.

**What you need to do:**

* **To run a framework agent**, send a POST to `/v1/{workspace_id}/agent-runs` with your service ID, chosen framework, and user message. Poll the returned run ID until status is no longer `running`.
* **Model resolution.** Both frameworks resolve the model from the version set's engage model preference. If the engage model is compatible with the target framework, it is used directly. If the engage model belongs to a different provider family (or is not set), the framework falls back to a platform default - a non-compatible model preference is never forwarded to a framework that cannot use it. No explicit model pin is required on either framework.
* **Version set configuration.** The `version_set` field (default `release`) selects which version set's agent configuration the run uses. Ensure your service has the intended version set configured.

</details>

<details>

<summary>Platform API: Fleet Status - Tool-Runner Fleet Observability (July 2026)</summary>

#### Fleet Status - Tool-Runner Fleet Observability <a href="#fleet-status-tool-runner-fleet-observability" id="fleet-status-tool-runner-fleet-observability"></a>

The fleet status endpoint now supports querying the tool-runner fleet in addition to the voice fleet, giving operators visibility into capacity for both the per-call voice fleet and the isolated background-tool fleet.

**What changed:**

* **New `fleet` query parameter.** The fleet status endpoint (`GET /v1/{workspace_id}/sessions/fleet-status`) now accepts an optional `fleet` query parameter. Pass `fleet=voice` (the default) to read the per-call voice fleet, or `fleet=tool-runner` to read the isolated background-tool fleet. Omitting the parameter returns the voice fleet, so existing callers are unaffected.
* **Same response shape.** Both fleet selections return the same response model - `fleet`, `namespace`, `ready`, `allocated`, `total`, `max_replicas`, `headroom`, and `by_state`. The `fleet` and `namespace` fields in the response echo which fleet was read, so consumers can confirm which fleet's data they received.
* **Tool-runner headroom.** When the tool-runner fleet has a configured capacity ceiling, the `headroom` field reports remaining isolated-unit slots (`max_replicas - allocated`). When the ceiling is not configured, `max_replicas` and `headroom` are null.

**What you need to do:**

* **No action required for existing integrations.** The default behavior is unchanged - calls without the `fleet` parameter continue to return voice fleet status.
* **To monitor tool-runner capacity**, pass `?fleet=tool-runner` to the fleet status endpoint.

</details>

<details>

<summary>Platform API: Cross-Provider Identity Linking for Invited Operators (July 2026)</summary>

#### Cross-Provider Identity Linking for Invited Operators <a href="#cross-provider-identity-linking-for-invited-operators" id="cross-provider-identity-linking-for-invited-operators"></a>

Operators who accepted a workspace invitation through a magic link or email one-time-password and later attempt to sign in with Google are now automatically linked to their existing identity, instead of receiving a 401 error.

**What changed:**

* **Automatic cross-provider identity linking.** When an invited operator first accepts an invitation through a magic link or email code, their identity is created under the email provider. If they later sign in with Google, the platform now detects the existing email-based identity and creates a linked Google identity for the same operator - so subsequent Google sign-ins resolve immediately without repeating the invitation flow.
* **Trust-gated linking.** The link is only created when Google is authoritative for the operator's mailbox. This means the Google account must either be managed by the email domain's own workspace administration (for corporate domains) or the mailbox must be Google-native (for consumer addresses). A consumer Google account carrying a third-party corporate email address is not linked, because Google's email verification in that case does not prove current inbox control.
* **Conflict detection.** If the operator's entity already has a different Google identity linked, the platform refuses the new link and logs the conflict rather than overwriting the existing binding. An administrator must resolve the conflict manually.
* **Audit logging.** Every cross-provider link emits an audit event recording the linked providers, email, and Google identity subject, so workspace administrators have full visibility into automated identity bindings.

**What you need to do:**

* **No action required.** Operators who previously encountered 401 errors when signing in with Google after accepting an invitation via magic link or email code can now sign in with Google directly. No configuration changes are needed.

</details>

<details>

<summary>Platform API: Source Change Routing - Document Withdrawal and Cursor Persistence (July 2026)</summary>

#### Source Change Routing - Document Withdrawal and Cursor Persistence <a href="#source-change-routing-document-withdrawal-and-cursor-persistence" id="source-change-routing-document-withdrawal-and-cursor-persistence"></a>

The intake system now supports document withdrawal when a connected data source reports a file has been trashed or removed, and persists change cursors so incremental polling resumes from the last committed position.

**What changed:**

* **Document withdrawal.** When a data source's change feed reports a file deletion, the platform tombstones the corresponding document by flipping its status to withdrawn with a timestamp and reason. The withdrawal is idempotent - re-delivered delete events are no-ops. The materialized customer data layer mirrors the withdrawal status.
* **Re-activation on re-upload.** If a previously withdrawn document is re-uploaded or restored from trash, the tombstone is automatically cleared and the document is treated as a new version. Documents move between active and withdrawn states without manual intervention.
* **Cursor persistence on data sources.** Each connected data source now tracks a change cursor, a last-synced timestamp, and a last-sync error. The cursor advances only after a poll's batch of changes is durably applied, so a crash resumes from the last committed position. Errors are recorded for observability without moving the cursor backward.

**What you need to do:**

* **No action required for existing data sources.** Document withdrawal and cursor persistence are handled internally. Existing data source configurations continue to work without changes.

</details>

<details>

<summary>Platform API: Folder-Listing Diff Deletion Fallback for Folder-Only Access (July 2026)</summary>

#### Folder-Listing Diff Deletion Fallback for Folder-Only Access <a href="#folder-listing-diff-deletion-fallback-for-folder-only-access" id="folder-listing-diff-deletion-fallback-for-folder-only-access"></a>

The intake connector's deletion detection now falls back to a folder-listing diff when the incremental change feed is unavailable, so data sources with folder-only access (no shared-drive membership) can still detect and withdraw removed files.

**What changed:**

* **Automatic fallback.** When the incremental change feed is unavailable (for example, when the service account has folder-level access but is not a member of the shared drive), the connector automatically falls back to comparing the current folder listing against previously recorded files. Removed files are withdrawn using the same tombstone mechanism as the change feed path.
* **Truncation guard.** If any folder listing is truncated by the per-sync file cap, the diff is skipped entirely. An incomplete listing is never treated as authoritative, and the skip is recorded as a sync error for operator visibility.
* **Blast-radius circuit breaker.** If a single sync would withdraw more than 50% of a source's known documents, the withdrawal is held for manual review rather than applied. The hold is recorded as a sync error so operators can investigate before documents are removed. This prevents a transient glitch or misconfiguration from wiping a knowledge base.
* **No baseline required.** Unlike the change feed (which needs a first run to establish a cursor), the folder-listing diff works immediately by comparing the current listing against the platform's recorded file inventory.

**What you need to do:**

* **No action required.** The fallback is automatic. Data sources that previously could not detect deletions due to folder-only access now have deletion detection enabled. If a held withdrawal appears as a sync error, review the source's folder contents before clearing the error.

</details>

<details>

<summary>Platform API: Incremental Change Detection for Drive-Based Data Sources (July 2026)</summary>

#### Incremental Change Detection for Drive-Based Data Sources <a href="#incremental-change-detection-for-drive-based-data-sources" id="incremental-change-detection-for-drive-based-data-sources"></a>

The intake connector for drive-based data sources now supports an incremental change feed that detects file creations, modifications, and deletions since a saved cursor. Previously, the connector could only list all files under a folder - it had no way to detect deletions because a removed file simply stops appearing in the listing.

**What changed:**

* **Incremental change feed.** The connector can now poll for changes since a cursor, receiving explicit per-file events for creates, modifications, trashes, and removals. This replaces the need to diff folder listings to detect deleted files.
* **Deletion detection.** Trashed and removed files are surfaced as explicit deletion events. The connector routes these to a withdrawal so downstream systems know the file is gone, without requiring a full folder re-scan.
* **Cursor management.** A baseline cursor can be fetched when a data source is first connected. On each subsequent poll, the connector receives an updated cursor to persist for the next cycle. If no changes have occurred, the previous cursor is returned unchanged.
* **Shared drive scoping.** When the data source is backed by a shared drive, change detection is scoped to that drive so only relevant changes are returned.
* **Pagination safety.** The change feed follows pagination internally with a bounded page limit, consistent with existing listing operations.

**What you need to do:**

* **No action required for existing data sources.** The incremental change feed is used internally by the connector to improve deletion handling. Existing data source configurations continue to work without changes.

</details>

<details>

<summary>Platform API: Epic SMART Backend Services JWKS Endpoint (July 2026)</summary>

#### Epic SMART Backend Services JWKS Endpoint <a href="#epic-smart-backend-services-jwks-endpoint" id="epic-smart-backend-services-jwks-endpoint"></a>

The connector runner now serves a public JWK Set endpoint for Epic SMART Backend Services integration. Epic fetches this endpoint to verify the client-assertion JWTs that the platform signs when requesting access tokens.

**What changed:**

* **New public endpoint: `GET /connectors/epic/jwks`.** Returns a JWK Set containing the public RSA key used for SMART Backend Services client assertions. The key identifier (`kid`) is the RFC 7638 JWK thumbprint, and the key is marked for RS384 signature use.
* **Signing key provisioned through infrastructure automation.** The RSA signing key is provisioned by infrastructure tooling and injected into the service through secret management. The service requires the key at startup - it will not start without it.
* **Cache-friendly response.** The endpoint returns cache headers allowing intermediaries and Epic to cache the JWK Set, reducing repeated fetches.

**What you need to do:**

* **No action required for existing integrations.** This endpoint is consumed by Epic during the SMART Backend Services token exchange flow. If you are setting up a new Epic integration, register the JWKS URL with your Epic environment as part of the SMART Backend Services configuration.

</details>

<details>

<summary>Platform API: Trigger Run History - Durable Execution Log for Triggers (July 2026)</summary>

#### Trigger Run History - Durable Execution Log for Triggers <a href="#trigger-run-history-durable-execution-log-for-triggers" id="trigger-run-history-durable-execution-log-for-triggers"></a>

Trigger execution history is now served from a durable run queue instead of the entity-event timeline. Each trigger fire produces a run record that tracks status, attempts, and results through its full lifecycle.

**What changed:**

* **New trigger run model.** The `GET /v1/{workspace_id}/triggers/{trigger_id}/runs` endpoint now returns purpose-built execution records instead of entity-event timeline entries. Each run includes an ID, trigger ID, fired event ID, source, status, attempt metadata, result text, and error details.
* **Updated response shape.** The run response fields have changed. Previous fields (`event_id`, `event_type`, `data`, `effective_at`) have been replaced with a richer model: `id`, `workspace_id`, `trigger_id`, `fired_event_id`, `source`, `input_override`, `status`, `attempt_count`, `max_attempts`, `next_attempt_at`, `claimed_by`, `claimed_at`, `lease_expires_at`, `result_text`, `error`, `created_at`, and `updated_at`.
* **Run statuses.** Each run progresses through a lifecycle: `queued`, `running`, `succeeded`, `failed`, or `dead`. Runs that exhaust their retry attempts without succeeding are moved to `dead` status.
* **Run sources.** The `source` field indicates how the trigger was fired: `cron` (scheduled), `manual` (user-initiated), `webhook`, or `event`.
* **Per-run input overrides.** Runs can carry optional per-run input overrides as a JSON object, allowing individual fires to customize the action input without changing the trigger definition.
* **Attempt tracking.** Each run tracks `attempt_count` and `max_attempts`. Runs that fail but have remaining attempts are automatically requeued. Runs that exhaust all attempts are dead-lettered with an error.
* **Idempotent enqueue.** Duplicate fire events for the same workspace and fired event produce only one run record. Retried enqueue attempts return the existing run rather than creating duplicates.

**What you need to do:**

* **Update integrations that consume trigger run history.** If you read trigger run data from `GET /triggers/{trigger_id}/runs`, update your code to use the new response shape. The previous `event_id`, `event_type`, `data`, and `effective_at` fields are no longer returned.
* **No action required for trigger creation or firing.** Existing trigger configurations and fire operations continue to work. The change affects only how execution history is returned.

</details>

<details>

<summary>Platform API: Document Embeddings in Intake Materializer (July 2026)</summary>

#### Document Embeddings in Intake Materializer <a href="#document-embeddings-in-intake-materializer" id="document-embeddings-in-intake-materializer"></a>

The intake materializer now computes document embeddings automatically during document-leg materialization. Previously, the `embedding` column was left empty for document datasets. Embeddings are now generated server-side during the merge step, so document rows are ready for vector search immediately after materialization.

**What changed:**

* **Automatic embedding computation.** When the materializer processes document datasets, it computes a vector embedding for each new or content-changed document as part of the merge operation. The embedding is derived from the document body text (truncated to fit the model's context window) and stored as an array of floats on the document row.
* **Incremental embedding.** Only new or content-changed documents receive an embedding computation. Documents whose content has not changed since the last materialization are skipped entirely - they are neither rewritten nor re-embedded. This keeps re-runs efficient.
* **Configurable embedding model.** The materializer accepts an `embedding_model` parameter that selects the embedding model used for vector computation. The default model produces 1024-dimension vectors suitable for English-language retrieval. Setting the parameter to empty skips embedding computation, leaving the column null - useful for parser-only iterations where you want to materialize document metadata without incurring embedding cost.
* **Input truncation.** Only the embedding input is truncated to fit the model's token window. The full document body is always stored without truncation, so no content is lost.

**What you need to do:**

* **No action required for existing integrations.** If you trigger materialization through the API or Developer Console, document datasets will now include embeddings automatically. To skip embeddings (for example, during a parser-only test run), pass an empty `embedding_model` parameter.
* **Re-materialize existing document datasets.** Previously materialized document datasets have null embeddings. Trigger a new materialization run to backfill embeddings for existing documents.

</details>

<details>

<summary>Platform API: Intake Materializations List Endpoint (July 2026)</summary>

#### Intake Materializations List Endpoint <a href="#intake-materializations-list-endpoint" id="intake-materializations-list-endpoint"></a>

A new endpoint returns per-dataset materialization status for a workspace, showing which datasets have been materialized into customer data tables and their current row counts.

**What changed:**

* **`GET /v1/{workspace_id}/intake/materializations`.** Lists each registered dataset alongside its customer data destination table status - `materialized` (with row count and last-write timestamp) or `not_materialized` (no table yet). The dataset list mirrors `GET /datasets` (the contracts registry) with the same pagination, sorting, and search parameters.
* **Response fields.** Each item includes `dataset` (string), `ingestion_mode` (string), `status` (`materialized` or `not_materialized`), `row_count` (integer, null when not materialized), and `last_materialized_at` (ISO-8601 timestamp, null when not materialized).
* **Pagination.** Supports `limit` (1-200, default 50), `continuation_token` (offset, default 0), `sort_by` (default `name`), and `search` (optional, max 200 characters). Returns `items`, `has_more`, and `continuation_token` in a standard paginated response envelope.
* **Graceful degradation.** If the destination catalog is unavailable (for example, in local development), all datasets report `not_materialized` rather than returning an error.
* **Auth.** Member read permission. Uses the same rate limiting as other read endpoints.

**What you need to do:**

* **No action required for existing integrations.** This is a new read-only endpoint. Use it to display materialization status in dashboards or to verify that datasets have been materialized after triggering a materialization run.

</details>

<details>

<summary>Platform API: Use Case Ownership Endpoints - Assign, Release, Get, List (July 2026)</summary>

#### Use Case Ownership Endpoints <a href="#use-case-ownership-endpoints" id="use-case-ownership-endpoints"></a>

Use case tenancy is now operator-assigned. A workspace claims ownership of a channel use case through explicit ownership endpoints rather than through the create proxy. The use case create and delete proxies have been retired.

**What changed:**

* **`PUT /v1/{workspace_id}/use-cases/{use_case_id}/ownership`.** Assign ownership of a use case to the current workspace. The use case must exist in the channel service. Idempotent - re-assigning an already-owned use case returns 200. Returns 404 if the use case does not exist, 409 if it is already owned by another workspace. Requires the Channel ManageOwnership permission (admin tier).
* **`DELETE /v1/{workspace_id}/use-cases/{use_case_id}/ownership`.** Release the current workspace's ownership of a use case. Returns 404 if this workspace does not own the use case, 409 if the use case still has an active service binding (unbind first). Requires Channel ManageOwnership permission.
* **`GET /v1/{workspace_id}/use-cases/{use_case_id}/ownership`.** Check whether the current workspace owns a specific use case. Returns the ownership record (use case ID and workspace ID) or 404. Requires Channel view permission.
* **`GET /v1/{workspace_id}/use-cases/ownership`.** List all use case IDs owned by the current workspace. Requires Channel view permission.
* **`POST /use-cases` removed.** The use case create proxy has been retired. Use cases are created directly in the channel service; platform records ownership separately.
* **`DELETE /use-cases/{use_case_id}` removed.** The use case delete proxy has been retired. Use cases are deleted directly in the channel service; release ownership in platform first.
* **New permission: `Channel.ManageOwnership`.** Admin-tier permission required for assign and release operations. Members cannot self-claim use cases.

**What you need to do:**

* **Update integrations that create use cases.** If you previously used `POST /use-cases` on the platform API to create use cases, create them directly in the channel service and then call `PUT /{use_case_id}/ownership` to assign ownership to your workspace.
* **Update integrations that delete use cases.** If you previously used `DELETE /use-cases/{use_case_id}`, release ownership first with `DELETE /{use_case_id}/ownership`, then delete the use case directly in the channel service.
* **Ensure admin credentials for ownership operations.** The assign and release endpoints require admin-tier credentials with the Channel ManageOwnership permission.

</details>

<details>

<summary>Platform API: Intake Materialization Endpoint (July 2026)</summary>

#### Intake Materialization Endpoint <a href="#intake-materialization-endpoint" id="intake-materialization-endpoint"></a>

A new endpoint lets you trigger materialization of curated intake snapshot datasets into workspace customer data tables on demand.

**What changed:**

* **`POST /v1/{workspace_id}/intake/materialize`.** Triggers the intake-to-customer-data materializer for the workspace. The materializer rescans the workspace's curated intake files and (re)materializes each snapshot dataset into workspace-scoped customer data tables. Every write is an idempotent keyed merge or full overwrite, so re-triggering is always safe.
* **Optional dataset scoping.** The request body accepts an optional `dataset` field (string). When provided, only the specified dataset is materialized. When omitted, all curated snapshot datasets for the workspace are materialized.
* **Response.** Returns `202 Accepted` with a `run_id` (integer). When the materializer is not configured in the environment, `run_id` is `null` and the call is a no-op.
* **Error handling.** If the materializer job fails to launch, the endpoint returns `502 Bad Gateway` with a descriptive error message.
* **Auth.** Uses the same authentication and rate limiting as `POST /intake/batches/{batch_id}/process`.

**What you need to do:**

* **No action required for existing integrations.** This is a new endpoint. Use it when you need to trigger materialization of curated intake data into customer data tables after upload and processing are complete.

</details>

<details>

<summary>Platform API: Async Tool Lifecycle Signaling - background_pending, tool_started SSE, Drain-First Guidance (July 2026)</summary>

#### Async Tool Lifecycle Signaling <a href="#async-tool-lifecycle-signaling" id="async-tool-lifecycle-signaling"></a>

The text conversation turn response now tells you whether the agent's answer is final or whether background work is still running. A new SSE event announces slow tools the instant they start, and the drain-first delivery contract is documented on the turn response.

**What changed:**

* **`background_pending` on turn responses.** `TurnResponse` (from `POST /v1/{workspace_id}/conversations/{conversation_id}/turns`) now includes a `background_pending` boolean field (default `false`). When `true`, the turn's `output` is only an acknowledgement that a tool is still running in the background - the definitive agent answer will arrive later, out-of-band. This field is set from the turn's internal state regardless of whether `include_tool_calls` was requested, so it is always authoritative.
* **`text.tool_started` SSE event.** The workspace event stream (`GET /v1/{workspace_id}/events/stream`) now emits a `text.tool_started` event the instant a slow tool begins executing - before the blocking window elapses. The event carries `conversation_id`, `tool_name`, `call_id` (format `bg:<task_id>`), and `depth`. The `call_id` matches the eventual `text.background_result` event, so a client can collapse the start and result into a single progress card.
* **Drain-first delivery contract documented.** The `background_pending` field description documents three ways to retrieve the final answer for a background-pending turn: re-issue `POST .../turns` with `poll=true` (a no-message drain), send the next user turn, or read the conversation back with `GET .../conversations/{id}`. The workspace event stream mirrors this activity for dashboard-style observers but is not the per-chat delivery path.

**What you need to do:**

* **Check `background_pending` on turn responses.** If your integration consumes turn responses, inspect `background_pending`. When `true`, do not treat `output` as the final answer - poll or send a follow-up turn to retrieve the completed result. Ignoring this field means your client may display a "still working" acknowledgement as if it were the agent's real answer.
* **Subscribe to `text.tool_started` for live progress (optional).** If your client already listens to the workspace SSE stream for `text.background_result`, you can now also listen for `text.tool_started` to show immediate progress when a tool begins. Match start and result events using the shared `call_id`.
* **No breaking changes.** `background_pending` defaults to `false`, preserving existing behavior for turns that complete synchronously.

</details>

<details>

<summary>Platform API: Per-Credential Rate Limiting on MCP Endpoint (July 2026)</summary>

#### Per-Credential Rate Limiting on MCP Endpoint <a href="#per-credential-rate-limiting-on-mcp-endpoint" id="per-credential-rate-limiting-on-mcp-endpoint"></a>

The MCP data-access endpoint (`/v1/mcp`) now enforces a per-(workspace, credential) sliding-window rate limit, protecting shared compute resources from runaway partner agents or tool-call loops.

**What changed:**

* **Per-credential throughput cap.** Each authenticated credential is allowed up to 120 requests per 60-second window against a given workspace's MCP surface. The limit is keyed on the verified credential, so one partner credential cannot exhaust resources for the entire workspace.
* **Standard rate-limit response.** When the limit is exceeded, the endpoint returns HTTP 429 with `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers. Clients should back off for the number of seconds indicated by `Retry-After`.
* **Fail-open behavior.** If the rate-limiting infrastructure is temporarily unavailable, requests are allowed through. The throughput cap never blocks access to the read surface due to infrastructure issues.

**What you need to do:**

* **No action required for well-behaved clients.** The 120 requests/minute limit is generous for normal agent usage (approximately 2 requests per second sustained). If your integration exceeds this rate, implement backoff logic using the `Retry-After` header value from the 429 response.

</details>

<details>

<summary>Platform API: Channel-Generic Conversation Lifecycle API - Start Outbound &#x26; Switch Channel (July 2026)</summary>

#### Channel-Generic Conversation Lifecycle API - Start Outbound & Switch Channel <a href="#channel-generic-conversation-lifecycle-api-start-outbound-and-switch-channel" id="channel-generic-conversation-lifecycle-api-start-outbound-and-switch-channel"></a>

The conversations endpoint now supports proactively starting outbound conversations on SMS and iMessage, switching an active conversation to a different channel, and per-turn channel attribution in conversation history.

**What changed:**

* **Outbound conversation start.** `POST /v1/{workspace_id}/conversations` accepts a `channel` field, a `recipient` (E.164 phone number), a `use_case_id` (resolves the sending number and routing server-side - never caller-supplied), and an optional `instruction` (steers the agent's opener). The platform dispatches the agent's opening message through the channel and returns the durable conversation. Requires `Conversation:StartOutbound` permission (member tier and above). The service must be owned by the workspace, and the use case must be owned by the workspace and service.
* **Only SMS and iMessage are wired for outbound today.** `channel` accepts `sms` or `imessage`. Any other channel (`email`, `whatsapp`, `voice`) returns `501`. The default `channel=web` remains an inbound create and is unaffected.
* **Channel switching.** `POST /v1/{workspace_id}/conversations/{conversation_id}/channel` re-keys the *same* durable conversation onto a different channel (`sms` or `imessage` today). The `conversation_id`, history, and plan are preserved; the channel, provider, and provider thread ID change. Accepts `recipient`, `use_case_id`, and `reason` (all required), plus optional `dispatch_opener` (default `false` - sends a first agent turn on the new channel) and optional `instruction`. An inbound reply on the new channel re-converges to the same conversation because both platform-api and agent-engine derive the routing key from `(channel, recipient, use_case_id)` via a shared builder (convergence is guaranteed by construction, not by an end-to-end handset test). Requires `Conversation:SwitchChannel` permission (member tier and above). Voice conversations cannot be switched.
* **Error responses.** Both endpoints reject external-user tokens with `403` and callers missing the required permission with `403`. An unsupported channel returns `501`; a missing `recipient` or `use_case_id` returns `422`; a service or use case not owned by the workspace (and, for the create endpoint, the service) returns `404`; and if outbound is not configured, both return `503`. For switching, the conversation being not found, already closed, or not switchable returns `404`; another active conversation already on the same `(channel, recipient)` returns `409`; `dispatch_opener=true` on a conversation with no service returns `422`; and if the opener dispatch itself fails the switch is still persisted and the response is `502`.
* **Per-turn channel attribution.** Each turn in conversation history now includes a `channel` field indicating which channel the turn occurred on, returned by both `GET /v1/{workspace_id}/conversations` and `GET /v1/{workspace_id}/conversations/{conversation_id}`. Conversations that switched channels render each turn on its originating channel. The field may be null on turns written before this feature and on internal turns.
* **New permissions.** `Conversation:StartOutbound` and `Conversation:SwitchChannel` are granted at member tier and above (same tier as `Channel:Send`). Viewer and operator roles do not carry them, and external users cannot start outbound conversations or switch channels.
* **World event.** A `conversation.channel_switched` event is emitted when a conversation changes channels, carrying the new channel and the reason for the switch.
* **Audit logging.** Channel switches are audit-logged with the target channel and reason.

**What you need to do:**

* **No action required for existing integrations.** The default `channel=web` on conversation creation preserves the existing behavior. Per-turn channel attribution adds a new nullable field to turn responses - existing consumers can ignore it.
* **To start outbound conversations,** supply `channel` (`sms` or `imessage`), `recipient`, and `use_case_id` on the create request. Ensure your API key's role has `Conversation:StartOutbound` permission (member tier and above).
* **To switch channels,** call the new `/{conversation_id}/channel` endpoint with the target channel, recipient, use case, and reason, and ensure your role has `Conversation:SwitchChannel` permission.

</details>

<details>

<summary>Platform API: Tool-Dispatch Lifecycle Axis &#x26; Write-Time Guardrails (June 2026)</summary>

#### Tool-Dispatch Lifecycle Axis & Write-Time Guardrails <a href="#tool-dispatch-lifecycle-axis-and-write-time-guardrails" id="tool-dispatch-lifecycle-axis-and-write-time-guardrails"></a>

A tool bound to a context-graph state (`ToolCallSpec`) gains a third orthogonal dispatch axis, **`lifecycle`**, alongside `execution` and `delivery`, and the context-graph version write endpoint now rejects two dead-end dispatch combinations.

**What changed:**

* **New `lifecycle` field: `coupled` | `independent` (default `independent`).** `independent` is the historical fire-and-forget behavior: a background task runs to completion and delivers its result regardless of what the conversation does next. `coupled` ties the task to the conversation's current commitment: if the conversation moves on (a new user turn, or it ends) before the task finishes, its result is cooperatively superseded and dropped instead of folded in late with a stale answer. Honored on the text path; voice keeps its own call-teardown rule. Stored sparse (a binding left at the default omits the field on the wire), so every existing context graph is byte-for-byte unchanged.
* **Write-time guardrails on context-graph version creation.** A tool bound `execution="background"` whose result is `delivery="queue"` on any axis is now rejected because a fire-and-forget result folded into the next turn never proactively reaches the user. A `background` tool in the **terminal state** is also rejected because the conversation ends after it, so there is no future turn to deliver the result into.

**What you need to do:**

* **No action required.** The default (`independent`) preserves existing behavior exactly. Opt a binding into `coupled` only when its result is meaningless once the conversation has moved on (for example, a side lookup that only informs the current turn). The OpenAPI spec exposes the new field, and `@amigo-ai/platform-sdk` (≥ v0.81.0) carries the type.

</details>

<details>

<summary>v0.9.505 - Platform API: Remove Channels API (SES Setup + Email Templates) (July 2026)</summary>

#### Remove Channels API (SES Setup + Email Templates) <a href="#remove-channels-api-ses-setup--email-templates" id="remove-channels-api-ses-setup--email-templates"></a>

The `/v1/{workspace_id}/channels` endpoint group has been removed. SES setup management and email template CRUD are no longer available through the Platform API.

**What changed:**

* **SES setup endpoints removed.** The following endpoints are no longer available:
  * `POST /v1/{workspace_id}/channels/ses-setup` (create)
  * `GET /v1/{workspace_id}/channels/ses-setup/{id}` (get with DNS refresh)
  * `POST /v1/{workspace_id}/channels/ses-setup/{id}/verify` (DNS verification)
  * `DELETE /v1/{workspace_id}/channels/ses-setup/{id}` (delete)
* **Email template endpoints removed.** The following endpoints are no longer available:
  * `POST /v1/{workspace_id}/channels/email/templates` (create)
  * `GET /v1/{workspace_id}/channels/email/templates` (list by use case)
  * `GET /v1/{workspace_id}/channels/email/templates/{id}` (get)
  * `PUT /v1/{workspace_id}/channels/email/templates/{id}` (update)
  * `DELETE /v1/{workspace_id}/channels/email/templates/{id}` (delete)
* **OpenAPI spec updated.** All channel-related paths, request models, and response models have been removed from the published OpenAPI specification.

**What you need to do:**

* **Remove any integrations using these endpoints.** If you have automation or tooling that calls the channels SES setup or email template endpoints, remove those calls. Requests to the removed paths will return 404.

</details>

<details>

<summary>v0.9.504 - Platform API: OAuth2 Client Last-Used Tracking (July 2026)</summary>

#### OAuth2 Client Last-Used Tracking <a href="#oauth2-client-last-used-tracking" id="oauth2-client-last-used-tracking"></a>

OAuth2 clients now record when they were last used to mint a token, giving workspace admins visibility into client activity without querying logs.

**What changed:**

* **`last_used_at` tracked on token exchange.** Every successful `client_credentials` token exchange now stamps the OAuth2 client with the current timestamp. The field is null until the client's first successful token request and is updated on every subsequent exchange.
* **Visible on the client record.** The `last_used_at` timestamp is available on the OAuth2 client object, so workspace admins can identify stale or unused clients.
* **No change to error behavior.** Failed authentication attempts (invalid client ID, wrong secret) do not update the timestamp.

**What you need to do:**

* **No action required.** The field is populated automatically on the next successful token exchange for each client. Existing clients will show `null` until they are next used.

</details>

<details>

<summary>v0.9.503 - Platform API: OAuth2 Scope Documentation on Channel Manager Routes (July 2026)</summary>

#### OAuth2 Scope Documentation on Channel Manager Routes <a href="#oauth2-scope-documentation-on-channel-manager-routes" id="oauth2-scope-documentation-on-channel-manager-routes"></a>

Every gated Channel Manager route now documents the exact OAuth2 scope it requires, making it straightforward to configure least-privilege OAuth2 clients.

**What changed:**

* **Required scope documented per route.** Each Channel Manager API route that enforces OAuth2 authorization now states the required scope in its description. This covers all channel operations (email, SMS, iMessage, ringless voicemail, outbound voice), setup management (Twilio, SendBlue, SES), phone number operations, compliance submissions (A2P, toll-free, CNAM, SHAKEN/STIR, regulatory bundles), use case management, and OAuth2 client administration.
* **Setup-scoped vs. resource-less scopes clarified.** Routes that check authorization against a specific setup state which setup the scope applies to (for example, the setup identified by a path parameter or query parameter). Routes that are not tied to a specific setup (such as creating a new setup or managing OAuth2 clients) note that they require a resource-less grant - only an exact-name grant satisfies them, not a wildcard scope.
* **Exact-name-only scopes identified.** Sensitive scopes such as `twilio-setup:credentials`, `twilio-setup:access-token`, and `clients:admin` are marked as exact-name grants that are not matched by wildcard scope patterns.
* **Bundle listing scope behavior documented.** The bundle listing route notes that it checks multiple compliance scopes and filters response content by which scopes the caller holds, rather than returning a blanket 403.

**What you need to do:**

* **No action required for existing integrations.** This is a documentation-only change. No authorization behavior has changed.
* **Review your OAuth2 client grants.** Use the per-route scope documentation to verify that your OAuth2 clients carry only the scopes they need. The scope tables in the Authentication guide list every scope with its access level and description.

</details>

<details>

<summary>v0.9.502 - Platform API: Production Eval Verdicts (July 2026)</summary>

#### Production Eval Verdicts <a href="#production-eval-verdicts" id="production-eval-verdicts"></a>

The same eval framework used for simulation testing is now available for production calls. Workspaces can define eval criteria that run against live completed calls and retrieve per-call verdicts with pass/fail status, rationale, and cited conversation turns.

**What changed:**

* **Production eval definitions.** A new set of CRUD endpoints lets you create, list, get, update, and delete eval definitions that apply to your workspace's live calls. Each definition specifies an eval type (assertion or metric), an eval key, and an expected outcome. Definitions can be scoped to a specific service or applied workspace-wide. Service-scoped definitions override workspace-wide definitions with the same key when evaluating calls for that service.
* **On-demand call evaluation.** A new `POST /v1/{workspace_id}/calls/{conversation_id}/evaluate` endpoint runs all active eval definitions against one completed call and persists the verdicts. The endpoint is synchronous - the caller waits while the judge processes. Deterministic assertions (transcript contains, tool called, final state) resolve instantly; AI judge assertions and metric evals invoke an AI model. Each eval that encounters an error produces its own error verdict rather than failing the entire evaluation.
* **Verdict retrieval.** A new `GET /v1/{workspace_id}/calls/{conversation_id}/eval-results` endpoint returns all persisted verdicts for a call. Each verdict includes status (passed, failed, pending, skipped, or error), optional numeric score, rationale, justification (for AI-evaluated metrics), and cited turn indices.
* **Idempotent re-evaluation.** Re-evaluating a call overwrites prior verdicts for the same eval keys rather than creating duplicates.
* **Shared eval semantics.** Production eval verdicts use the same judge, transcript format, assertion evaluators, and metric threshold comparisons as simulation evals. A verdict means the same thing whether it came from a simulation or a live call.
* **Assertion kinds supported.** `transcript_contains` / `must_contain`, `transcript_not_contains` / `must_not_contain`, `tool_called`, `final_state`, and `llm_judge` (the default for unrecognized kinds).
* **Metric eval thresholds.** Metric evals support `gte`/`min`, `lte`/`max`, `equals`, and `contains` comparisons, as well as bare value equality and no-threshold (value-only) mode.

**Permissions:**

| Operation                          | Required Permission |
| ---------------------------------- | ------------------- |
| List, get definitions              | Service view        |
| Create, update, delete definitions | Service update      |
| Evaluate a call                    | Service update      |
| Get eval results                   | Service view        |

**What you need to do:**

* **No action required for existing integrations.** Production evals are opt-in. No existing behavior is changed.
* **To get started:** Create one or more production eval definitions for your workspace, then call the evaluate endpoint on any completed call to see verdicts.

</details>

<details>

<summary>v0.9.501 - Platform API: Slot Provider Display Name Resolution (July 2026)</summary>

#### Slot Provider Display Name Resolution <a href="#slot-provider-display-name-resolution" id="slot-provider-display-name-resolution"></a>

Scheduling slots now automatically resolve the provider's display name from the practitioner projection, matching the existing behavior for appointment participant displays.

**What changed:**

* **Provider display resolved on slots.** When a scheduling slot carries a provider identifier, the platform now resolves it against the practitioner projection to populate the provider display name on the entity snapshot. Previously, slots that did not carry an explicit provider name from the source connector showed a blank provider display.
* **Connector-supplied names preserved.** If the source connector already provides a provider name (for example, through a vendor-specific extension), that value takes priority. The resolved name is used only when the connector-supplied display is empty or missing.
* **Provider identifier fallback for legacy data.** Slots from connectors that historically emitted the provider identifier under a different extension key are now resolved correctly without waiting for a re-sync. The projection reads from both the current and legacy extension keys, preferring the current one.
* **Shared resolution logic.** The display resolution logic for both appointment participants and slot providers is now consolidated, ensuring consistent behavior across entity types.

**What you need to do:**

* **No action required.** Slot entities in the world model will automatically show resolved provider display names on their next projection refresh. Existing slots with connector-supplied provider names are unaffected.

</details>

<details>

<summary>v0.9.500 - Platform API: Per-Tenant Intake Upload Isolation by Default (July 2026)</summary>

#### Per-Tenant Intake Upload Isolation by Default <a href="#per-tenant-intake-upload-isolation-by-default" id="per-tenant-intake-upload-isolation-by-default"></a>

Intake uploads are now routed to isolated per-tenant storage by default.

**What changed:**

* **Per-tenant isolation is now the default.** All workspaces route intake uploads to isolated per-tenant storage by default. Previously, per-tenant isolation had to be enabled platform-wide.
* **Dynamic exclusions.** Workspaces that still depend on the legacy shared storage layout are excluded through a per-upload evaluation rather than a static configuration list, so exclusions can be updated without a service redeploy.
* **No change to reads.** Read operations resolve each file from where it was stored, so files uploaded under either layout remain readable.

**What you need to do:**

* **No action required for most integrations.** Upload and download behavior is unchanged from the caller's perspective.
* **If you manage tenant exclusions:** Contact your platform administrator to update exclusion rules.

</details>

<details>

<summary>v0.9.499 - Platform API: Extended Zoom Bot Session Duration and Optional Duration Limits (July 2026)</summary>

#### Extended Zoom Bot Session Duration and Optional Duration Limits <a href="#extended-zoom-bot-session-duration-and-optional-duration-limits" id="extended-zoom-bot-session-duration-and-optional-duration-limits"></a>

Zoom meeting bot sessions now support up to four hours by default, and the maximum duration limit is now optional rather than required.

**What changed:**

* **Default session duration extended to four hours.** Zoom bot sessions now default to a four-hour maximum duration, up from the previous 30-minute default. This supports longer meetings such as all-hands, training sessions, and extended clinical consultations without requiring callers to specify a custom duration.
* **Maximum duration is now optional.** The `maxDurationSeconds` field on bot creation requests is now optional. When omitted, sessions run without a fixed time limit - the bot remains active until the meeting ends or the session is explicitly stopped. When provided, the value is validated against the platform's internal maximum.
* **Bot response schema updated.** The `maxDurationSeconds` field in the bot response model is now nullable. Bots created without a duration limit return `null` for this field instead of a default value.
* **Simplified duration validation.** Session duration validation no longer references separate configurable maximum and production maximum settings. Duration requests are validated against a single internal maximum (four hours). Requests exceeding this limit or exceeding the speech-to-text provider's session limit are rejected.

**What you need to do:**

* **No action required for most integrations.** Existing integrations that specify `maxDurationSeconds` continue to work unchanged. The extended default benefits integrations that previously relied on the default duration.
* **If you parse the bot response:** The `maxDurationSeconds` field can now be `null`. Update any client code that assumes this field is always present as an integer.
* **If you set custom durations:** The maximum allowed value is now four hours (14,400 seconds). Requests exceeding this limit will be rejected.

</details>

<details>

<summary>v0.9.498 - Platform API: PHI/PII Scrubbed from Channel Manager Logs (July 2026)</summary>

#### PHI/PII Scrubbed from Channel Manager Logs <a href="#phi-pii-scrubbed-from-channel-manager-logs" id="phi-pii-scrubbed-from-channel-manager-logs"></a>

Webhook and send logs in the channel manager no longer include protected health information (PHI) or personally identifiable information (PII) such as phone numbers, email addresses, or message content.

**What changed:**

* **Phone numbers removed from send logs.** Outbound SMS send operations no longer log the recipient phone number.
* **Webhook payloads no longer logged verbatim.** Inbound SMS, iMessage, and email webhook handlers no longer log raw request bodies or form parameters that may contain message content or contact information. Inbound SMS webhooks now log only the parameter key names present in the request, not their values.
* **Phone numbers removed from iMessage webhook logs.** Inbound and delivery event logs for iMessage no longer include sender or recipient phone numbers. Authentication gate context no longer carries phone numbers.
* **Email addresses removed from inbound email logs.** Inbound email webhook logs no longer include sender addresses, recipient addresses, or mail-from values in warning and drop messages (DMARC failures, spam filtering, sender mismatches, missing headers, and unresolved recipients).
* **Unparseable webhook payloads logged only on failure.** For iMessage webhooks, raw payload bodies are now logged only when the payload cannot be parsed, rather than on every request. This limits exposure of message content in logs to cases that require debugging.

**What you need to do:**

* **No action required.** This is a security hardening change. No API behavior, request formats, or response formats have changed. If you rely on platform logs for debugging webhook issues, note that contact details and message content are no longer present in log entries - use conversation records and message detail endpoints instead.

</details>

<details>

<summary>v0.9.497 - Platform API: Semantic Memory Consolidation (July 2026)</summary>

#### Semantic Memory Consolidation <a href="#semantic-memory-consolidation" id="semantic-memory-consolidation"></a>

The memory system now includes a semantic consolidation layer that integrates per-conversation episodic observations into a durable, trajectory-aware patient model. Instead of relying solely on the latest observation per dimension, the platform synthesizes longitudinal patterns across conversations into a single integrated narrative the agent loads and reasons from.

**What changed:**

* **Trajectory-aware patient models.** The platform now runs a nightly consolidation job that reads recent episodic observations for each patient and produces an integrated semantic model capturing trajectories - rising anxiety across multiple contacts, shifting motivation, eroding engagement - not just the latest data point. This is the information that no single conversation reveals.
* **Bounded input, constant cost.** Consolidation uses a bounded window: the patient's current model plus a configurable number of recent observations per dimension. A patient with five years of history costs the same to consolidate as one with five weeks. There is no unbounded history scan.
* **Mandatory lineage.** Every consolidated model cites the specific observation IDs it was synthesized from. The chain from the model back to source conversations is always traceable - "why does the system believe this?" resolves to specific observations.
* **Safety signal preservation.** Safety-relevant signals (hopelessness, self-harm, suicidal ideation, crisis) are never lost during consolidation. Any safety signal present in the observation window is carried into the consolidated model explicitly and faithfully, with high confidence.
* **Incremental processing.** Only patients with new episodic observations since their last consolidation are processed. Successfully consolidated patients advance a per-entity watermark; failed patients retain their previous watermark and retry on the next run.
* **Error isolation.** Individual patient consolidation failures do not block other patients. Failed entities are recorded in a queryable error log and retry automatically on the next run. If all entities in a run fail (indicating a systemic issue), the run raises an alert.
* **Standard data path.** Consolidated models flow through the same enrichment path as episodic observations, so the agent receives the integrated model through its existing loading mechanism with newest-wins resolution.

**What you need to do:**

* **No action required.** Semantic consolidation runs automatically. Patients with conversation history will progressively receive integrated models that surface longitudinal patterns. The agent will load the consolidated model alongside existing enrichment data without configuration changes.
* **If you consume memory observations downstream:** Consolidated observations carry `layer` information distinguishing them from per-conversation episodic observations. The consolidated model includes lineage references back to its source observations.

</details>

<details>

<summary>v0.9.496 - Platform API: Per-Client Rate Limiting on Channel Manager Authenticated Routes (July 2026)</summary>

#### Per-Client Rate Limiting on Channel Manager Authenticated Routes <a href="#per-client-rate-limiting-on-channel-manager-authenticated-routes" id="per-client-rate-limiting-on-channel-manager-authenticated-routes"></a>

All authenticated Channel Manager API routes now enforce per-client sliding-window rate limits. Each OAuth2 client is rate-limited independently per route, preventing any single integration from monopolizing API capacity.

**What changed:**

* **Per-client rate limits on all authenticated routes.** Every authenticated Channel Manager endpoint now enforces a per-client request cap over a sliding time window. Rate limits are scoped to the OAuth2 client ID and the specific route, so one client's traffic does not count against another client's allowance, and limits on one endpoint do not affect other endpoints.
* **429 responses with Retry-After header.** When a client exceeds its rate limit, the API returns HTTP 429 (Too Many Requests) with a `Retry-After` header indicating how many seconds the client should wait before retrying. The `Retry-After` value reflects the actual time remaining in the current window.
* **Fail-open on limiter unavailability.** If the rate limiting backing store is temporarily unreachable, requests are allowed through rather than rejected. A limiter outage never takes down the API - it degrades gracefully by admitting all traffic until the store recovers.
* **Rate limit tiers by route category.** Limits are calibrated per route based on expected usage patterns:
  * **High-throughput read endpoints** (get message, get email, get attachment, get setup status, get credentials, get campaign, get verification, get brand registration): 600 requests per minute per client
  * **Send endpoints** (send email, send SMS, send iMessage): 300-600 requests per minute per client
  * **List and search endpoints** (list messages, list emails, list events, list templates, list phone numbers, list bundles, list voicemails): 240 requests per minute per client
  * **Outbound voice phone number selection**: 1,200 requests per minute per client
  * **Moderate-throughput endpoints** (send ringless voicemail, mint access token, send opt-in, opt-out removal, email template writes, get regulation, list available numbers): 60-120 requests per minute per client
  * **Setup and use case management** (create/update/delete setup, create/delete use case, phone number provisioning, compliance submissions, phone number assignment): 10-30 requests per minute per client
  * **OAuth2 client management** (create, update, delete, rotate secret): 10 requests per minute per client

**What you need to do:**

* **No action required for typical usage.** The rate limits are set well above normal integration traffic patterns. Most integrations will not be affected.
* **If you run high-volume batch operations:** Review your send and list call volumes against the limits above. If you anticipate exceeding the per-minute caps, implement client-side throttling or spread requests across a longer window.
* **Handle 429 responses.** If your integration does not already handle HTTP 429, add retry logic that respects the `Retry-After` header. This is standard HTTP practice and ensures your integration recovers gracefully from rate limit events.

</details>

<details>

<summary>v0.9.495 - Platform API: Appointment Participant Resolution and Scheduled Rehydration (July 2026)</summary>

#### Appointment Participant Resolution and Scheduled Rehydration <a href="#appointment-participant-resolution-and-scheduled-rehydration" id="appointment-participant-resolution-and-scheduled-rehydration"></a>

EHR-connected appointments now resolve participants by client vs non-client classification, correctly handle group and couples sessions, and automatically rehydrate upcoming appointment detail on a scheduled cadence.

**What changed:**

* **Client vs non-client participant mapping.** Appointments synced from connected clinical systems now distinguish between client appointments (patient sessions) and non-client appointments (administrative, personal, or front-desk blocks). Patient references are attached only to client appointments. Non-client appointments carry practitioner and location participants but no patient reference, preventing phantom patient-less appointments from appearing in scheduling views and patient timelines.
* **Group and couples session support.** Appointments with multiple attendees (group therapy, couples sessions) now resolve all attending patients as participants rather than only the primary patient. The primary patient and all additional attendees are included in the appointment's participant list, with deduplication so no patient is referenced twice. Attendees explicitly excluded from a specific session are omitted.
* **Practitioner reference accuracy.** Practitioner references on appointments now use the same identifier space as the staff roster, so practitioner participants resolve correctly to their roster entities. Previously, a different grouping identifier was used that did not match any roster entry.
* **Scheduled upcoming appointment rehydration.** The platform now periodically re-fetches full detail for all appointments in the upcoming window (the next several days). The incremental sync only enriches appointments that have changed, so stable upcoming appointments previously carried incomplete data (missing modality, service type, and practitioner resolution). The scheduled rehydration ensures that all upcoming appointments carry full detail - including practitioner, patient, location, and service type - regardless of whether they changed recently.
* **Scheduled contact rehydration.** Patient contact information (email, phone) is now periodically re-synced for the full patient roster. Previously, contact-only edits on stable patient records were not picked up by the incremental sync. The scheduled rehydration ensures that contact changes are reflected without waiting for other patient data to change.

**What you need to do:**

* **No action required.** The changes are automatic. Upcoming appointments will progressively gain full participant and service detail through the scheduled rehydration. Patient contact information will stay current through the roster rehydration. Non-client appointments will no longer produce patient references.
* **If you filter appointments by patient participant:** Appointments that previously carried a spurious patient reference (non-client blocks, admin time) will no longer have one. If your integration relied on every appointment having a patient participant, update your filtering to handle appointments with only practitioner and location participants.

</details>

<details>

<summary>v0.9.494 - Platform API: Mid-Session Patient Binding for Voice and Text (July 2026)</summary>

#### Mid-Session Patient Binding for Voice and Text <a href="#mid-session-patient-binding-for-voice-and-text" id="mid-session-patient-binding-for-voice-and-text"></a>

Patients resolved during a conversation - not just at session start - are now bound to the session so the session-ended event carries the correct patient entity. This restores memory and continuity for returning patients identified mid-call.

**What changed:**

* **Patient lookup binding.** When a patient lookup returns exactly one match (a sole strict match by name and date of birth), the matched patient is now bound to the session as the primary entity. Previously, only patients created during a call or resolved by phone at session start were bound. This caused returning patients - the common case - to be silently dropped from the session-ended event, which meant downstream memory extraction and continuity loading never received their data.
* **Text session binding.** Text conversations now participate in the same mid-session patient binding as voice calls. A patient resolved by creation or sole-match lookup during a text chat is bound to the session and included in the session-ended event.
* **First resolution wins.** The session binds the first patient resolution only. Ambiguous multi-match lookups (where more than one patient matches) are not bound, avoiding incorrect entity associations when duplicate records exist.
* **Session-ended event accuracy.** The session-ended event now carries the patient entity resolved during the conversation (via any resolution path) rather than relying solely on the entity snapshot captured at session creation time. This applies to both voice and text channels.

**What you need to do:**

* **No action required.** The change is automatic. Sessions that previously emitted session-ended events without a patient link (because the patient was identified mid-call rather than at session start) will now include the correct patient entity. Memory extraction and continuity loading for returning patients will resume without configuration changes.

</details>

<details>

<summary>v0.9.493 - Platform API: Enriched Eval Transcripts and Rationale on All Eval Kinds (July 2026)</summary>

#### Enriched Eval Transcripts and Rationale on All Eval Kinds <a href="#enriched-eval-transcripts-and-rationale-on-all-eval-kinds" id="enriched-eval-transcripts-and-rationale-on-all-eval-kinds"></a>

Simulation eval results now include rationale and turn references on every eval kind, and the transcript fed to AI judges and metrics has been enriched with tool call arguments, results, available tools, empathy tier, and terminal markers.

**What changed:**

* **Rationale and references on all assertion kinds.** Deterministic assertions (transcript contains, tool called, final state) now produce a human-readable rationale and cite the turn index that supports the verdict. Previously, only AI judge assertions and justified metric evals returned rationale and references. For example, a transcript-contains assertion that passes returns a rationale like "Found 'appointment confirmed' in turn 5" with `references: [5]`. A tool-called assertion that fails returns "Tool schedule\_appointment was never called. Tools used: lookup\_patient, check\_availability." with an empty references list.
* **AI judge assertions now return turn references.** The AI judge prompt now asks the model to return a `references` list of turn indices alongside `passed`, `score`, and `rationale`. References are validated to be in-range turn indices; out-of-range or non-integer entries are dropped.
* **Enriched transcript for judges and metrics.** The indexed transcript now includes tool call arguments and results (size-capped per field), available tools per turn, empathy tier, and a terminal marker on the last turn. This gives AI judges and metrics visibility into tool correctness, available-but-unused tools, emotional tone, and where the conversation ended - not just utterances.
* **Case content in metric prompts.** When a simulation run includes case content (the scenario the conversation was run against), that context is now passed into justified metric prompts. Goal-oriented metrics can score against what the simulated patient was trying to do, not just the raw exchange.

**What you need to do:**

* **No action required.** Existing eval definitions continue to work. Rationale and references are populated automatically on all new eval results. Clients that read eval results will see the new `rationale` and `references` fields populated where they were previously empty or absent.
* **If you display eval results:** Consider surfacing the rationale and references fields for deterministic assertions - they now provide the same explanatory detail that was previously available only for AI-evaluated metrics.

</details>

<details>

<summary>v0.9.492 - Platform API: Batch Document Extraction and Processing Manifest (July 2026)</summary>

#### Batch Document Extraction and Processing Manifest <a href="#batch-document-extraction-and-processing-manifest" id="batch-document-extraction-and-processing-manifest"></a>

Document batch processing now dispatches a single extraction run per batch instead of one run per file. This eliminates gateway timeouts that occurred when starting processing on large document batches.

**What changed:**

* **Single extraction run per batch.** When you call Start Processing on a document batch, the platform now fires one extraction job run that processes all received files in that batch. Previously, each file triggered its own run, which serialized on the request path and caused 504 gateway timeouts for batches with many files.
* **Processing manifest endpoint.** A new endpoint returns the per-file extraction parameters for all still-received files in a batch. The extraction job fetches this manifest at the start of its run and processes each file in sequence. The manifest includes per-file metadata (file type, content type, hash, size) and dataset-wide extraction configuration from the contract. Filenames are deliberately excluded from the manifest to keep PHI off the wire.
* **Resumable and idempotent.** Start Processing now accepts batches in both `ready` and `processing` status, so you can re-run it to pick up files that were not dispatched on a previous attempt. Terminal batches return a 409. If no received files remain, the endpoint returns the current batch state without dispatching.
* **Snapshot and CSV batches unchanged.** Snapshot and CSV files still process in version order with chained write-backs. Only document batches use the new single-run path.
* **Per-file fault isolation in batch runs.** If one file in a batch fails extraction, the remaining files continue processing. A best-effort failed verdict is recorded for the errored file so the batch can still roll up to a terminal state.

**What you need to do:**

* **No action required.** The change is transparent to API consumers. The Start Processing endpoint accepts the same request shape and returns the same response. Large document batches that previously timed out should now complete successfully.
* **If you poll batch status:** No changes needed. The batch still transitions through `processing` to `completed` or `failed` as each file reaches a terminal verdict.

</details>

<details>

<summary>v0.9.491 - Platform API: Persisted Background Tool Completions (July 2026)</summary>

#### Persisted Background Tool Completions <a href="#persisted-background-tool-completions" id="persisted-background-tool-completions"></a>

Background tool completions are now persisted as structured tool-call records and correlated to the original dispatch by a stable task-based key. Previously, a background tool's result was surfaced only as a live streaming card and a prose system message - if you re-read the conversation later, the tool call appeared stuck at "running" with no recorded outcome.

**What changed:**

* **Structured persistence.** When a background tool completes (success or failure), the platform writes a durable tool-call record containing the tool name, result, success status, execution duration, and an optional error message. This record appears alongside other tool calls in conversation history.
* **Stable correlation key.** The completion record, the live streaming card, and the background-result event all share the same identifier derived from the dispatch's task ID. A reader can pair the completion to the original "running" dispatch entry by that shared key without parsing prose.
* **Dispatch placeholder preserved.** The original "running" dispatch entry is intentionally left in place. The correlation model is pairing, not suppression - both the dispatch and its completion are visible in the tool-call timeline.
* **Both re-entry paths covered.** Completions are persisted whether they arrive during an active streaming session or are drained at the start of the next REST request.

**What you need to do:**

* **No action required.** Background tool completions are now automatically persisted. Existing conversations with in-flight background tools will record completions going forward. Previously completed background tools that were only surfaced as prose system messages are not retroactively backfilled.

</details>

<details>

<summary>v0.9.490 - Platform API: EHR Write-Back Endpoint for Patient Creation and Appointment Booking (July 2026)</summary>

#### EHR Write-Back Endpoint for Patient Creation and Appointment Booking <a href="#ehr-write-back-endpoint-for-patient-creation-and-appointment-booking" id="ehr-write-back-endpoint-for-patient-creation-and-appointment-booking"></a>

The connector runner now exposes a synchronous, authenticated HTTP surface for writing patient and appointment data back to the connected EHR. This endpoint is designed for upstream automation agents that need to create patients, book initial consultations, reschedule appointments, and cancel appointments - with safety guarantees against duplicate writes and double-bookings.

**What changed:**

* **Synchronous EHR write-back endpoint.** Three new operations are available: book (create patient + book appointment), reschedule, and cancel. Each operation accepts a structured request body and returns a typed response indicating the outcome.
* **HMAC-SHA256 authentication.** Every request must include an HMAC-SHA256 signature over the raw request body, using the same signature scheme as existing webhook integrations. The signing secret is bound to exactly one workspace - requests targeting a different workspace are rejected.
* **Idempotency keys.** Every write request requires a caller-supplied idempotency key. The platform claims the key before the first external write, stores the terminal response, and returns the cached result on retries. Concurrent duplicate requests see an in-flight status instead of triggering duplicate writes.
* **Partial failure recovery.** If patient creation succeeds but the subsequent appointment booking fails, the response includes the new patient ID with a partial status. Retrying with the same idempotency key skips patient creation and completes only the booking, preventing duplicate patients.
* **Live calendar conflict checks.** Before every booking or reschedule, the endpoint performs a live re-check of the provider's calendar at the requested time. If the provider is already booked, the request is refused with a re-offer status rather than creating a double-booking.
* **Office room selection.** For reschedules that involve an in-person modality, the endpoint automatically selects a free physical office at the new time. If no office is available, telehealth fallback or re-offer behavior applies based on the requested modality.
* **Cohort assignment on patient creation.** New patients can be assigned to one or more cohorts at creation time. Assigning a cohort that carries an intake packet triggers the EHR to email that packet automatically as a side effect of creation.
* **Gated writes.** Real EHR mutations are gated behind a safety flag. Until the flag is enabled (pending compliance sign-off), the endpoint operates in dry-run mode - it runs all validation and conflict checks and reports what would be written, without mutating the EHR. Callers can also request dry-run mode explicitly.
* **Audit events.** Every completed write (book, reschedule, cancel) emits a durable audit event for compliance tracking.
* **Improved telehealth detection for appointments.** When a physical room is explicitly specified on an appointment, the room is now authoritative for determining telehealth status. The telehealth flag is set based on whether the room is the telehealth room. Free-text and service code fallbacks apply only when no room is specified. This prevents in-person bookings from being incorrectly flagged as telehealth when they share a service code with telehealth appointments.

**What you need to do:**

* **No action required for existing integrations.** The async event-driven write-back path is unchanged. New-patient create-then-book flows that require idempotency guarantees should use the new synchronous endpoint instead of the async path.
* **Configure credentials for the new endpoint.** If you plan to use the synchronous write-back surface, provision the HMAC signing secret and workspace binding through your deployment configuration.

</details>

<details>

<summary>v0.9.489 - Platform API: Stable Document Resolution for Connector Syncs (July 2026)</summary>

#### Stable Document Resolution for Connector Syncs <a href="#stable-document-resolution-for-connector-syncs" id="stable-document-resolution-for-connector-syncs"></a>

Connector-synced files are now resolved to existing documents using a stable external identity rather than filenames. This prevents duplicate documents when the same file is synced repeatedly and correctly handles file renames between syncs.

**What changed:**

* **Identity-based document resolution.** Each connector-synced file now carries its source system's permanent file identifier. On each sync, the platform looks up whether a document already exists for that identity. If found, the file is ingested as a new version of the existing document. If not found, a new document is created at version 1 with the external identity recorded.
* **No duplicate documents on re-sync.** Previously, connector syncs could create duplicate documents if the same external file appeared in multiple sync cycles. The platform now enforces uniqueness on the combination of workspace, dataset, source type, and external file identifier, so each external file maps to exactly one document.
* **Renamed files matched correctly.** Because resolution is based on the source system's stable identifier rather than the filename, a file that is renamed in the source system is still matched to its existing document. The filename is updated as display metadata without creating a new document.
* **Provenance fields on file list.** The Files list response now includes `source_type` and `source_file_id` on each file row, showing the origin and external identifier of the document the file belongs to. These fields are null for manual uploads and snapshot/CSV files.
* **Manual uploads unchanged.** Files uploaded through the console continue to use the explicit document selection model. No external identity is recorded for manual uploads.

**What you need to do:**

* **No action required for existing documents.** Existing documents created before this change default to `manual` source type with no external identifier. They continue to work as before.
* **Update API consumers that read file list responses.** If you parse the file list response, two new optional fields (`source_type` and `source_file_id`) are now present on each file row. These fields are nullable and do not affect existing integrations.

</details>

<details>

<summary>v0.9.488 - Platform API: Recursive Subfolder Discovery for Drive Intake Sources (July 2026)</summary>

#### Recursive Subfolder Discovery for Drive Intake Sources <a href="#recursive-subfolder-discovery-for-drive-intake-sources" id="recursive-subfolder-discovery-for-drive-intake-sources"></a>

Drive-based intake sources now discover files across the entire folder tree under each mapped folder, not just the top-level folder. The per-sync file cap has been increased to accommodate larger folder structures.

**What changed:**

* **Recursive file discovery.** When a source folder is synced, the platform now walks the entire subtree under the configured folder. Files in nested subfolders are discovered alongside files in the root folder. One mapped folder corresponds to one dataset, and the full tree is included in the discovery batch.
* **Subfolder traversal, not ingestion.** Subfolders are traversed to find files but are not themselves treated as files. Only non-folder items appear in the resulting batch.
* **Cycle protection.** The traversal detects and skips folders that have already been visited, preventing infinite loops caused by shortcuts or circular folder structures.
* **Increased per-sync file cap.** The maximum number of files included in a single sync has been raised from 500 to 1,000 to accommodate deeper folder trees. Files beyond this cap are still logged and skipped rather than silently dropped.

**What you need to do:**

* **Review your mapped folders.** If your source folders contain subfolders, files in those subfolders will now be included in sync batches automatically. Ensure that nested content is appropriate for your dataset.
* **Check file counts.** If your folder tree contains more than 1,000 files, only the first 1,000 are included per sync. Contact support if you need a higher cap or plan to onboard larger folder trees.

</details>

<details>

<summary>v0.9.487 - Platform API: OAuth2 Scope Enforcement on Channel Manager Setup and Client Routes (July 2026)</summary>

#### OAuth2 Scope Enforcement on Channel Manager Setup and Client Routes <a href="#oauth2-scope-enforcement-on-channel-manager-setup-and-client-routes" id="oauth2-scope-enforcement-on-channel-manager-setup-and-client-routes"></a>

The channel manager now enforces OAuth2 scopes on all setup creation routes (email, SMS, and iMessage) and all OAuth2 client management routes (create, update, delete, and secret rotation). Previously, these routes did not require scope authorization. Now, each request must carry a bearer token with the appropriate scope.

**What changed:**

* **Setup creation routes.** Creating a new email setup, SMS setup, or iMessage setup now requires the corresponding setup-create scope on the bearer token. Requests without the required scope receive a 403 response.
* **OAuth2 client management routes.** Creating, updating, deleting, and rotating the secret of an OAuth2 client now require the client administration scope. Requests without the required scope receive a 403 response.
* **Consistent authorization model.** These routes now follow the same scope enforcement pattern used by the read, update, and delete routes added in the previous release, completing scope coverage across the channel manager API surface.

**What you need to do:**

* **Ensure your OAuth2 tokens include the required scopes.** If you create channel setups or manage OAuth2 clients through the channel manager API, your bearer token must carry the appropriate scope for the operation.
* **Handle 403 responses.** API clients should be prepared to receive 403 responses when the token does not grant the required scope for setup creation or client management operations.

</details>

<details>

<summary>v0.9.486 - Platform API: Per-Call Isolation Attach Authorization (July 2026)</summary>

#### Per-Call Isolation Attach Authorization <a href="#per-call-isolation-attach-authorization" id="per-call-isolation-attach-authorization"></a>

The per-call media isolation path now enforces that the media transport attaches to the same isolated server that was allocated for the call. This closes an authorization gap where a valid assignment could be used to attach media to a different server than the one the platform assigned.

**What changed:**

* **Route pinning on allocation.** When the platform allocates an isolated server for a call, the allocated route identity is written back onto the call's runtime assignment. This binding lets the attach step verify that the incoming media stream reached the correct server.
* **Attach-time route validation.** When a media stream connects to an isolated server, the platform now checks that the stream's route identity matches the route pinned on the assignment. Streams that reach a different server than the one assigned are rejected.
* **Conflict detection.** If an assignment is already pinned to a different route (indicating two allocations raced onto a single call), the platform fails the call leg rather than silently routing media to the wrong destination.
* **Idempotent re-pinning.** Telephony provider retries that carry the same route identity are treated as no-ops, so duplicate callbacks do not cause conflicts.
* **Best-effort for legacy paths.** Calls on the legacy (non-isolated) path that do not have a runtime assignment skip route pinning gracefully. Storage and infrastructure failures fail closed - the call leg is failed rather than proceeding without verification.

**What you need to do:**

* **No action required.** This change is transparent to API consumers. Calls routed through per-call isolation now have stronger media-routing integrity guarantees with no changes to API surface or call behavior.

</details>

<details>

<summary>v0.9.485 - Platform API: OAuth2 Scope Enforcement on Channel Manager Read Routes (July 2026)</summary>

#### OAuth2 Scope Enforcement on Channel Manager Read Routes <a href="#oauth2-scope-enforcement-on-channel-manager-read-routes" id="oauth2-scope-enforcement-on-channel-manager-read-routes"></a>

The channel manager now enforces OAuth2 scopes and setup-level access checks on all by-ID read, update, and delete routes for email, SMS, and iMessage resources. Previously, a valid bearer token could access any resource regardless of its associated setup. Now, each request is validated against the token's granted scopes and the setup that owns the resource.

**What changed:**

* **Email routes.** The get email, get email body, get email attachment, get email raw, get email template, update email template, and delete email template endpoints now require the appropriate email scope and verify that the token has access to the setup that owns the resource. Requests with insufficient scope or mismatched setup access receive a 403 response.
* **SMS routes.** The get SMS message and get SMS message attachment endpoints now require the SMS read scope and verify setup-level access through the associated use case. Requests with insufficient scope or mismatched setup access receive a 403 response.
* **iMessage routes.** The get iMessage and get iMessage media endpoints now require the iMessage read scope and verify setup-level access. Requests with insufficient scope or mismatched setup access receive a 403 response.
* **New error responses.** All affected endpoints now document 401 (missing, expired, or invalid bearer token) and 403 (token lacks the required scope or access to the setup) responses.

**What you need to do:**

* **Ensure your OAuth2 tokens include the required scopes.** If you access channel manager resources by ID, your token must carry the appropriate read or write scope for the channel (email, SMS, or iMessage) and must have access to the setup that owns the resource.
* **Handle 401 and 403 responses.** API clients should be prepared to receive 401 responses for authentication failures and 403 responses when the token does not grant access to the requested resource's setup.

</details>

<details>

<summary>v0.9.484 - Platform API: EHR Connector Write-Back - Patient Lookup, Appointment Cancellation, and Idempotent Booking (July 2026)</summary>

#### EHR Connector Write-Back - Patient Lookup, Appointment Cancellation, and Idempotent Booking <a href="#ehr-connector-write-back-patient-lookup-appointment-cancellation-and-idempotent-booking" id="ehr-connector-write-back-patient-lookup-appointment-cancellation-and-idempotent-booking"></a>

The connector runner's outbound write-back pipeline now supports patient lookup by contact information, appointment cancellation, appointment rescheduling, and idempotent appointment creation. Write-back remains gated per connector pending sign-off and service-line configuration.

**What changed:**

* **Patient lookup by contact.** The connector can now search the EHR patient list with contact columns (phone and email) to resolve whether a caller is an existing patient or a new patient. This supports the agent's new-vs-existing patient identification flow.
* **Appointment cancellation.** Outbound write-back now handles appointment cancellation events. When the agent cancels an appointment, the connector sends the cancellation to the EHR using the appointment's native identifier. Cancellation failures surface as structured errors rather than silent no-ops.
* **Appointment rescheduling.** Booking an appointment with an existing native appointment identifier updates the existing appointment rather than creating a new one.
* **Idempotent appointment creation.** Retried appointment creates check whether an appointment already exists for the same provider and start time before sending. If a match is found, the retry is treated as a no-op to prevent double-booking.
* **Appointment booking validation.** Appointment writes now require a room and service line (practice-specific scheduling attributes) to be present. Writes missing these attributes are skipped with a structured reason rather than sent as incomplete requests.
* **Patient creation validation.** Patient creation now requires an assigned provider in addition to first and last name. Writes missing the provider are skipped with a structured reason.
* **Booking attribute resolution.** Free-text availability slot titles are resolved into the structured scheduling attributes (room, service line, duration, telehealth flag) that the EHR booking form requires. This bridges the gap between the availability data the agent sees and the structured data the EHR expects.
* **Structured error handling for bookings.** Appointment booking responses are now inspected for EHR-reported validation errors, which are surfaced as structured failures rather than treated as successful no-ops.

**What you need to do:**

* **No action required.** Write-back remains gated and does not fire in production until explicitly enabled per connector. These changes prepare the write-back pipeline for production enablement once sign-off and service-line configuration are complete.

</details>

<details>

<summary>v0.9.483 - Platform API: Intake Batch Processing (July 2026)</summary>

#### Intake Batch Processing <a href="#intake-batch-processing" id="intake-batch-processing"></a>

Intake batches - groups of files discovered during a source sync - can now be listed, inspected, and processed through dedicated endpoints.

**What changed:**

* **List batches.** A new `GET /intake/batches` endpoint returns paginated batches for the workspace. Supports filtering by source ID and sorting by creation time or status.
* **Get batch detail.** A new `GET /intake/batches/{batch_id}` endpoint returns a single batch with its associated files.
* **Process a batch.** A new `POST /intake/batches/{batch_id}/process` endpoint starts processing a batch that is in `ready` status (returns 202). Batches in any other status return 409 Conflict.
* **Sequential snapshot processing.** Snapshot and CSV files within a batch process in version order - each file's completion triggers the next, since change-data-capture depends on the prior curated baseline. Document files are independent and process in parallel.
* **Automatic batch roll-up.** When every file in a batch reaches a terminal verdict (curated, rejected, or failed), the batch status rolls up automatically to `completed` or `failed`. This happens as each file's async processing job writes back its status.
* **Status write-back integration.** The existing file status write-back endpoint now advances the parent batch when a batch-sourced file reaches a terminal verdict - chaining the next snapshot version and rolling up the batch status. Batch advancement is best-effort; a transient failure in chaining does not affect the file's committed status.

**What you need to do:**

* **To process synced files in bulk**, call `GET /intake/batches` to find batches in `ready` status, then `POST /intake/batches/{batch_id}/process` to start processing.
* **No changes to existing upload or sync workflows.** Direct file uploads and source sync continue to work as before. Batches are created automatically during source sync.

</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.concurrence.com/api-reference/change-logs/amigo-api/amigo-api-history-07.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.
