diff --git a/CLAUDE.md b/CLAUDE.md
index c392c55..eb2f24e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -149,6 +149,35 @@ Raises:
- Prefer region comments for grouping methods in files that already use them.
- End edited Markdown and YAML files with a trailing newline.
+## What this package actually ships (and what it does not)
+
+This client installs **`ondewo/csi` only**. It contains **no** `ondewo/nlu`, `ondewo/s2t` or
+`ondewo/t2s` `*_pb2` modules — `pyproject.toml` declares `ondewo-nlu-client==7.0.2`,
+`ondewo-s2t-client` and `ondewo-t2s-client`, and the csi protos reference those packages'
+descriptors at import time. Verified against the installed `.dist-info/RECORD`: this dist claims
+zero files under `ondewo/nlu/`.
+
+That matters when someone reasons about descriptor-pool collisions. The sibling client
+`ondewo-vtsi-client-python` **does** vendor foreign protos (55 files under `ondewo/nlu`, plus
+`ondewo/{s2t,t2s,sip,qa}`), so *it* has to be regenerated in lockstep with the service clients. This
+one does not — bump it only when the `ondewo/csi` surface itself changes. Do not "helpfully" add
+vendored nlu protos here; it would create exactly the duplicate-file-in-pool crash the current
+layout avoids.
+
+Consumers pin this repo by git rev, not by PyPI version: `ondewo-vtsi` and `ondewo-csi` both pin
+rev **`23b568fa`** (the `CALL_ENDED` control-status regen) in their `pyproject.toml`. Never rebase or
+force-push a commit that a pin references.
+
+## Jenkins — never trigger a multibranch scan or branch indexing
+
+**NEVER trigger a Jenkins multibranch scan or branch indexing.** Do not call a multibranch/folder job's
+`build`, `scan`, or reindex endpoints, click "Scan Repository Now" / "Build Now" on a folder, run
+`p4 scan`, or use any API/CLI that reindexes branches or scans the repository. A scan/reindex runs across
+**every** branch, consumes CI resources, and can kick off unintended builds and deploys.
+
+If a branch is not building — it was not discovered, or its job is marked `buildable: false` / orphaned —
+**report it and stop**. Let the user or a Jenkins admin adjust branch-discovery/config or rename the branch
+to the convention. Never force a build by scanning or reindexing.
## Release gotchas (hard-won this session)
These bit us during the 6.14.0 release. Keep them in mind when releasing.
diff --git a/Makefile b/Makefile
index b4bbda5..5817784 100644
--- a/Makefile
+++ b/Makefile
@@ -143,7 +143,7 @@ create_async_services: ## Create async services for all synchronous services
cp "$$file" "$$dir/async_$$filename"; \
done; \
for file in "$$dir"/async_*.py; do \
- perl -i -pe 'unless(/def stub/){ s/^([[:space:]]*)def /$$1async def /g; s/self\.stub/await self.stub/g; s/\(BaseServicesInterface\)/(AsyncBaseServicesInterface)/g; s/base_services_interface/async_base_services_interface/g; s/import BaseServicesInterface/import AsyncBaseServicesInterface/g; }' \
+ perl -i -pe 'unless(/def stub/){ s/^([[:space:]]*)def /$$1async def /g; s/self\.stub/await self.stub/g; s/\(BaseServicesInterface\)/(AsyncBaseServicesInterface)/g; s/base_services_interface/async_base_services_interface/g; s/import BaseServicesInterface/import AsyncBaseServicesInterface/g; s/core\.services_interface/core.async_services_interface/g; s/import ServicesInterface/import AsyncServicesInterface/g; s/\((?SetControlStatus that playback of the buffered audio remainder has resumed"""
+ PLAYBACK_DONE: _ControlStatus.ValueType # 7
+ """Status reported by the client to the server via SetControlStatus that playback of an utterance has completed"""
+ CALL_ENDED: _ControlStatus.ValueType # 8
+ """Status reported by ondewo-sip to the server via SetControlStatus that the phone call has ENDED and the server must reset ALL per-call state in place (persistent-listener model). It is a command, not a state: it is never pushed onto the control stream"""
class ControlStatus(_ControlStatus, metaclass=_ControlStatusEnumTypeWrapper):
"""Control status."""
@@ -60,6 +72,18 @@ EMERGENCY_STOP: ControlStatus.ValueType # 1
"""Status that control stream needs to stop immediately"""
VAD_START_OF_SPEECH: ControlStatus.ValueType # 2
"""Status that voice activity detection detected start of speech"""
+BARGE_IN: ControlStatus.ValueType # 3
+"""Status that the caller started speaking while the bot was speaking (barge-in): playback pauses and the un-played audio remainder is buffered"""
+RESUME_PLAYBACK: ControlStatus.ValueType # 4
+"""Status sent by the server to the client to resume playback of the buffered audio remainder after a false interruption (no committed transcription final arrived within the false interruption timeout)"""
+DISCARD_REMAINDER: ControlStatus.ValueType # 5
+"""Status sent by the server to the client to discard the buffered audio remainder because the interruption committed as a real turn"""
+PLAYBACK_RESUMED: ControlStatus.ValueType # 6
+"""Status reported by the client to the server via SetControlStatus that playback of the buffered audio remainder has resumed"""
+PLAYBACK_DONE: ControlStatus.ValueType # 7
+"""Status reported by the client to the server via SetControlStatus that playback of an utterance has completed"""
+CALL_ENDED: ControlStatus.ValueType # 8
+"""Status reported by ondewo-sip to the server via SetControlStatus that the phone call has ENDED and the server must reset ALL per-call state in place (persistent-listener model). It is a command, not a state: it is never pushed onto the control stream"""
global___ControlStatus = ControlStatus
class _ControlMessageServiceName:
@@ -1153,6 +1177,18 @@ class S2sStreamResponse(google.protobuf.message.Message):
DETECT_INTENT_RESPONSE_FIELD_NUMBER: builtins.int
SYNTHESIZE_RESPONSE_FIELD_NUMBER: builtins.int
SIP_TRIGGER_FIELD_NUMBER: builtins.int
+ UTTERANCE_ID_FIELD_NUMBER: builtins.int
+ CHUNK_INDEX_FIELD_NUMBER: builtins.int
+ LAST_CHUNK_FIELD_NUMBER: builtins.int
+ TURN_EPOCH_FIELD_NUMBER: builtins.int
+ utterance_id: builtins.str
+ """Optional. Identifier of the utterance this response belongs to. All chunks of one synthesized utterance share the same utterance_id"""
+ chunk_index: builtins.int
+ """Optional. Zero-based index of this chunk within the utterance identified by utterance_id"""
+ last_chunk: builtins.bool
+ """Optional. If true, this is the last chunk of the utterance identified by utterance_id"""
+ turn_epoch: builtins.int
+ """Optional. Monotonic turn epoch of the conversation turn that produced this response. Used by consumers to fence stale audio of an interrupted turn"""
@property
def detect_intent_response(self) -> ondewo.nlu.session_pb2.DetectIntentResponse:
"""full NLU detect intent response"""
@@ -1171,9 +1207,13 @@ class S2sStreamResponse(google.protobuf.message.Message):
detect_intent_response: ondewo.nlu.session_pb2.DetectIntentResponse | None = ...,
synthesize_response: ondewo.t2s.text_to_speech_pb2.SynthesizeResponse | None = ...,
sip_trigger: global___SipTrigger | None = ...,
+ utterance_id: builtins.str = ...,
+ chunk_index: builtins.int = ...,
+ last_chunk: builtins.bool = ...,
+ turn_epoch: builtins.int = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["detect_intent_response", b"detect_intent_response", "response", b"response", "sip_trigger", b"sip_trigger", "synthesize_response", b"synthesize_response"]) -> builtins.bool: ...
- def ClearField(self, field_name: typing.Literal["detect_intent_response", b"detect_intent_response", "response", b"response", "sip_trigger", b"sip_trigger", "synthesize_response", b"synthesize_response"]) -> None: ...
+ def ClearField(self, field_name: typing.Literal["chunk_index", b"chunk_index", "detect_intent_response", b"detect_intent_response", "last_chunk", b"last_chunk", "response", b"response", "sip_trigger", b"sip_trigger", "synthesize_response", b"synthesize_response", "turn_epoch", b"turn_epoch", "utterance_id", b"utterance_id"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["response", b"response"]) -> typing.Literal["detect_intent_response", "synthesize_response", "sip_trigger"] | None: ...
global___S2sStreamResponse = S2sStreamResponse
@@ -1298,14 +1338,18 @@ class ControlStreamResponse(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
CONTROL_STATUS_FIELD_NUMBER: builtins.int
+ EPOCH_FIELD_NUMBER: builtins.int
control_status: global___ControlStatus.ValueType
"""Control status"""
+ epoch: builtins.int
+ """Monotonic barge-in epoch/sequence number so control status transitions are correlatable with the S2sStreamResponse turn_epoch and a second barge-in during a resumed remainder can never be coalesced away"""
def __init__(
self,
*,
control_status: global___ControlStatus.ValueType = ...,
+ epoch: builtins.int = ...,
) -> None: ...
- def ClearField(self, field_name: typing.Literal["control_status", b"control_status"]) -> None: ...
+ def ClearField(self, field_name: typing.Literal["control_status", b"control_status", "epoch", b"epoch"]) -> None: ...
global___ControlStreamResponse = ControlStreamResponse
diff --git a/pyproject.toml b/pyproject.toml
index 42e345c..8d7c61d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,7 +33,7 @@ dependencies = [
"loguru",
"numpy",
"ondewo-client-utils>=3.2.0",
- "ondewo-nlu-client",
+ "ondewo-nlu-client==7.0.2",
"ondewo-s2t-client",
"ondewo-t2s-client",
"requests",
@@ -54,6 +54,7 @@ dev = [
"python-dotenv",
"setuptools",
"typing_extensions",
+ "types-requests",
"pytest-cov",
"pytest-asyncio",
]
diff --git a/tests/unit/client/test_keycloak.py b/tests/unit/client/test_keycloak.py
index 23c7d8b..bd57bd1 100644
--- a/tests/unit/client/test_keycloak.py
+++ b/tests/unit/client/test_keycloak.py
@@ -152,7 +152,7 @@ def _token_body(access_token: str, refresh_token: str, expires_in: int) -> Dict[
def _build_provider(
transport: FakeTransport,
- token_expiration_in_s: int | None = None,
+ token_expiration_in_s: Optional[int] = None,
) -> KeycloakTokenProvider:
"""Construct a `KeycloakTokenProvider` wired to the fake transport and shared test fixtures.
@@ -1055,7 +1055,7 @@ def _build_background_provider(
transport: FakeTransport,
clock: Dict[str, float],
stop_event: ScriptedEvent,
- token_expiration_in_s: int | None = None,
+ token_expiration_in_s: Optional[int] = None,
) -> KeycloakTokenProvider:
"""Construct a provider wired to an injected clock + scripted event, background thread off.
diff --git a/uv.lock b/uv.lock
index 24add85..e4a9e85 100644
--- a/uv.lock
+++ b/uv.lock
@@ -3975,6 +3975,8 @@ dev = [
{ name = "setuptools", version = "83.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "soundfile", version = "0.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "soundfile", version = "0.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "types-requests", version = "2.32.4.20260107", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "types-requests", version = "2.33.0.20260712", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "typing-extensions" },
]
@@ -3993,7 +3995,7 @@ requires-dist = [
{ name = "mypy-protobuf", marker = "extra == 'dev'" },
{ name = "numpy" },
{ name = "ondewo-client-utils", specifier = ">=3.2.0" },
- { name = "ondewo-nlu-client" },
+ { name = "ondewo-nlu-client", specifier = "==7.0.2" },
{ name = "ondewo-s2t-client" },
{ name = "ondewo-t2s-client" },
{ name = "pre-commit", marker = "extra == 'dev'" },
@@ -4008,39 +4010,30 @@ requires-dist = [
{ name = "setuptools" },
{ name = "setuptools", marker = "extra == 'dev'" },
{ name = "soundfile", marker = "extra == 'dev'", specifier = ">=0.10.3.post1" },
+ { name = "types-requests", marker = "extra == 'dev'" },
{ name = "typing-extensions", marker = "extra == 'dev'" },
]
provides-extras = ["dev"]
[[package]]
name = "ondewo-nlu-client"
-version = "6.14.0"
+version = "7.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "dataclasses-json" },
{ name = "google-api-core", version = "2.30.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "google-api-core", version = "2.31.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "grpcio", version = "1.80.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "grpcio", version = "1.81.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
- { name = "grpcio-reflection", version = "1.80.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "grpcio-reflection", version = "1.81.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
- { name = "grpcio-tools", version = "1.80.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "grpcio-tools", version = "1.81.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "loguru" },
{ name = "ondewo-client-utils" },
{ name = "polling" },
{ name = "protobuf" },
- { name = "python-dateutil" },
- { name = "regex", version = "2026.1.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "regex", version = "2026.6.28", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "requests", version = "2.32.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
- { name = "setuptools", version = "82.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "setuptools", version = "83.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/27/b5/3351e021c0421689d95a3e872b1b6c637e98d29a469bf5b578e7a168ef7d/ondewo_nlu_client-6.14.0.tar.gz", hash = "sha256:62ff5b02e829bd946822c7cdd6cf323f26438ed2d97a7aaa5554805588226fb3", size = 382977, upload-time = "2026-07-07T16:19:26.928Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/0a/e2/7472b521a5a13ef2e1f5e83e3d471e6264ed5620f5bac8419b0d18c8f024/ondewo_nlu_client-7.0.2.tar.gz", hash = "sha256:0056d0e0ee9d91d101dfade051297a1662dc1247e4fe1f8a240d47ae51258578", size = 411455, upload-time = "2026-08-08T14:03:39Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/42/80/3c9d2bafde077152a7cc0efab22bd23a6da44cf397942c3ff873742bc2f1/ondewo_nlu_client-6.14.0-py2.py3-none-any.whl", hash = "sha256:8b2172a7059f81d3a4c5d310f8bfe53d1347c7828f17a21bbbf31b47fa75cc19", size = 441118, upload-time = "2026-07-07T16:19:25.079Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/dc/e632cedddf6e4011c819ea044ffbd53094920f0cc0f9793a60b84d431fa8/ondewo_nlu_client-7.0.2-py3-none-any.whl", hash = "sha256:0d0e58e78d5e496ec1d15266e1b3af38d9b0a9d08618faee1cd1635d5fbdd60b", size = 461789, upload-time = "2026-08-08T14:03:35.379Z" },
]
[[package]]
@@ -6283,6 +6276,42 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/1f/ec5caf72c2e3b688ca3927e0979a04ddad19e1afc4bf1c199bd743e0f419/types_protobuf-7.34.1.20260518-py3-none-any.whl", hash = "sha256:a0a5337413347166439c0e07cbc26c6164d091401c6f01b1dfd8cdb966c4dd8f", size = 85992, upload-time = "2026-05-18T06:01:45.696Z" },
]
+[[package]]
+name = "types-requests"
+version = "2.32.4.20260107"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version > '3.9' and python_full_version < '3.10'",
+ "python_full_version <= '3.9'",
+]
+dependencies = [
+ { name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" } },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" },
+]
+
+[[package]]
+name = "types-requests"
+version = "2.33.0.20260712"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.15'",
+ "python_full_version == '3.14.*'",
+ "python_full_version == '3.13.*'",
+ "python_full_version == '3.12.*'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "urllib3", version = "2.7.0", source = { registry = "https://pypi.org/simple" } },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/db/51/703318f7b7be8bee126ec13bf615050f932d0179b8784420f3a0199cc769/types_requests-2.33.0.20260712.tar.gz", hash = "sha256:2141b67ab534a5c5cd2dac5034f2a35f42e699c5bf185eee608c5246a069d7fb", size = 25084, upload-time = "2026-07-12T05:14:20.455Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/e7/010c87f559e216d83f9dc51e939633fd0d0ead3377340181ab0e223cd3b5/types_requests-2.33.0.20260712-py3-none-any.whl", hash = "sha256:de027e28c171d3da529689cbfa023b0b4eab188c8dfa22fd834eebd2cee6e7bb", size = 21392, upload-time = "2026-07-12T05:14:19.616Z" },
+]
+
[[package]]
name = "typing-extensions"
version = "4.16.0"