---
title: "Update Runtime Policy"
method: PATCH
path: "/api/chats/{chat_id}/runtime-policy"
tags: ["chats"]
---

# Update Runtime Policy

`PATCH /api/chats/{chat_id}/runtime-policy`

## Request

- Base URL: `https://app.thinqos.com` (the document declares no server; this is the origin it was published from)
- URL: `https://app.thinqos.com/api/chats/{chat_id}/runtime-policy`
- Auth: none declared

Update conversation-level runtime fan-out policy.

TOS-2393: this can no longer set the per-chat/per-payer spend figure -
see ``UpdateRuntimePolicyRequest``'s docstring. Use
``PUT/DELETE /api/chats/{chat_id}/my-spend-limit`` for your own cap.
It CAN set ``max_root_container_spend_usd`` (a different, tree-wide
figure - finding #3), but only overwrites it when the request body
actually included that key, never by the field's Python default.

## Path parameters

- `chat_id` string, uuid, required

## Headers

- `x-impersonate-identity-id` string, nullable

## Request body

- UpdateRuntimePolicyRequest: Room-wide fan-out policy, plus the tree-wide container spend ceiling. TOS-2393: deliberately carries NO per-chat/per-payer spend field. It used to accept `max_chat_spend_usd`, but that let any caller of this general-purpose endpoint silently rewrite the room-wide spend figure as a side effect of changing responder fan-out. Per-payer spend caps now have their own dedicated, payer-scoped write path - `PUT/DELETE /api/chats/{chat_id}/my-spend-limit` (`UpdateMySpendLimitRequest` below) - which is the ONLY way to set that figure. `ChatService.update_runtime_policy` carries the per-payer map through from existing stored state unconditionally, so an extra `max_chat_spend_usd` key in a stale client's request body is simply ignored (Pydantic's default `extra="ignore"`), not silently destructive. That silence has a cost worth knowing about: because this body is `extra="ignore"` rather than `extra="forbid"`, a caller still sending `max_chat_spend_usd` gets a 200 and no effect. That is exactly how the production room-UX smoke broke in TOS-2393 - it PATCHed a `0` cap here to force a blocked turn, the key was dropped, and the room ran uncapped. `test_smoke_runtime_policy_matches_rest_contract` (backend/tests/test_agent_config_contract_parity.py) now fails the PR that removes a field from this body while a hard-coded payload still sends it. TOS-2394 removed the legacy scalar entirely, so there is no longer any stored figure for this endpoint to carry or to wipe. `max_root_container_spend_usd` is a DIFFERENT figure - the tree-wide container ceiling, not the per-chat cap - and review finding #3 caught that an earlier pass removed it here with no replacement write path anywhere, freezing every chat's ceiling at whatever it already held. It is intentionally OPTIONAL (`None` default) rather than carrying the always-defaulted `Field(default=...)` shape the fan-out fields use below: `update_runtime_policy` only overwrites the stored ceiling when this key was actually present in the request body (checked via `body.model_fields_set`, not `is not None` - the latter can't tell "omitted" from "explicitly set to null to clear it"), so a client that only changes fan-out settings and never mentions this field can't wipe it by omission - the same failure mode the fan-out-only design above exists to prevent for the chat-level scalar.
  - `max_responders` integer
  - `max_chain_depth` integer
  - `top_k_claimants` integer
  - `max_concurrent_generations` integer
  - `max_concurrent_generations_per_provider` integer
  - `max_root_container_spend_usd` number, nullable

## Response `200`

Successful Response

- ChatDetail
  - `id` string, uuid, required
  - `title` string, nullable, required
  - `container_kind` string, required
  - `status` string, required
  - `description` string, nullable
  - `default_agent_identity_id` string, uuid, nullable
  - `participants` ParticipantData[]
    - `identity_id` string, uuid, required
    - `display_name` string, required
    - `kind` string, required
    - `role` string, required
    - `status` string, required
    - `avatar_url` string, nullable
    - `joined_at` string, date-time, nullable
    - `left_at` string, date-time, nullable
    - `archived_at` string, date-time, nullable
    - `is_pinned` boolean
  - `messages` MessageData[]
    - `id` string, uuid, required
    - `chat_id` string, uuid, required
    - `sender_id` string, uuid, nullable, required
    - `sender` SenderSummary: Minimal identity info for attribution on a message or participant.
      - `id` string, uuid, required
      - `display_name` string, required
      - `kind` string
    - `role` string
    - `content` string
    - `model` string, nullable
    - `provider` string, nullable
    - `duration_ms` integer, nullable
    - `is_deleted` boolean
    - `has_attachments` boolean
    - `metadata` object, nullable
    - `created_at` string, date-time, nullable
    - `revised_at` string, date-time, nullable
    - `attachments` AttachmentData[]
      - `id` string, uuid, required
      - `file_name` string, required
      - `file_type` string, required
      - `mime_type` string, required
      - `file_size_bytes` integer, nullable, required
      - `created_at` string, date-time, nullable, required
    - `compression_summary` CompressionSummary: Structured marker for a `role="system"` compaction-summary message. Surfaced on `MessageData.compression_summary` so the UI can render the "earlier conversation summarized" divider without parsing raw metadata.
      - `messages_compressed` integer, required
      - `summary_model` string, required
  - `started_at` string, date-time, nullable
  - `updated_at` string, date-time, nullable
  - `extraction_excluded` boolean
  - `mind_context_disabled` boolean
  - `hidden_before_message_id` string, uuid, nullable
  - `capture_disposition` CaptureDispositionSummary: TOS-3758: this viewer's capture mailbox state for one captured conversation. ``using_connection_default`` is the field the UI renders. False means this conversation has diverged from the capture connection's archive/read policy and will no longer follow changes to it - a state that existed before this ticket but was completely invisible, and had no undo. ``can_reset`` is separate from ``not using_connection_default`` on purpose: an unbound source capture can only be handed back to a default when the caller can supply that source's policy, so the client must not offer a control that would no-op.
    - `source` string, required
    - `using_connection_default` boolean, required
    - `can_reset` boolean, required
    - `override_reason` string, nullable
  - `facilitation_mode` 'natural' | 'facilitated'
  - `facilitator_identity_id` string, uuid, nullable
  - `alternate_facilitator_identity_ids` string[]
  - `max_responders` integer
  - `max_chain_depth` integer
  - `top_k_claimants` integer
  - `max_concurrent_generations` integer
  - `max_concurrent_generations_per_provider` integer
  - `max_root_container_spend_usd` number, nullable
  - `viewer_max_chat_spend_usd` number, nullable
  - `viewer_chat_spend_usd` number, nullable
  - `viewer_model_spend_usd` number, nullable
  - `viewer_agent_fees_usd` number, nullable
  - `has_active_recipients` boolean
  - `lifecycle` 'live' | 'concluded'
  - `source` string, nullable
  - `source_display_name` string, nullable
  - `source_icon_url` string, nullable
  - `source_recognized` boolean, nullable

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/thinqos/apis/thinqos-api.md) · [All operations](https://skmtc.dev/thinqos/apis/thinqos-api/llms.txt) · [OpenAPI document](https://skmtc.dev/thinqos/apis/thinqos-api/revisions/ce2c2ce318b1?raw)
